From 8f0e011a07049436ab3533208b60c1d1ca417840 Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Tue, 20 Oct 2026 09:19:09 +0000 Subject: [PATCH] =?UTF-8?q?U4=20t=5F69687afc:=20Assets-Bereich=20Unity-art?= =?UTF-8?q?ig=20ausgebaut=20=E2=80=94=20Kategorien-Leiste=20(Alle=20+=20Gr?= =?UTF-8?q?uppen=20aus=20LibraryBrowser,=20Summe=20=3D=20Gesamtzahl),=20Su?= =?UTF-8?q?che=20Name+ID,=20Zaehler=20"N=20von=20M=20sichtbar",=20Zuletzt?= =?UTF-8?q?=20benutzt=20(cfg=20[library]=20zuletzt=5Fbenutzt,=20Chip=20+?= =?UTF-8?q?=20Ansicht),=20Kacheln=20mit=2096-px-Vorschau=20+=20ID=20+=20Na?= =?UTF-8?q?me,=20Hover=20vergroessert=20die=20ID,=20fehlende=20GLB=20=3D?= =?UTF-8?q?=20abgedunkeltes=20Bild=20+=20orange=20Badge=20"Modell=20fehlt"?= =?UTF-8?q?=20(GlbMeshStore.fehlt),=20Klick=20=3D=20auswaehlen=20(Signal?= =?UTF-8?q?=20object=5Fselected=20genau=201x=20je=20Wechsel),=20Doppelklic?= =?UTF-8?q?k=20=3D=20Werkzeug=20starten=20wie=20bisher=20(nochmal=20=3D=20?= =?UTF-8?q?abbrechen),=20Spaltenzahl=20breitenabhaengig=20(180=20px=20je?= =?UTF-8?q?=20Kachel,=20ueber=20ui=5Fscale),=20Dock-Groesse=20bleibt=20in?= =?UTF-8?q?=20[fenster]=20gemerkt=20(U1).=20Klapp-Gruppen=20+=20Alle=20auf?= =?UTF-8?q?/zu=20+=20gepinnte=20Masse=20(Suchfeld=2036,=20Kachel=20126,=20?= =?UTF-8?q?Thumb=2096)=20unveraendert.=20Suite=20tests/test=5Fassets=5Fpan?= =?UTF-8?q?el.gd=2075=20checks=20ALL=20OK;=20test=5Flibrary=5Fbrowser=5Fui?= =?UTF-8?q?=2030,=20test=5Flibrary=20101,=20test=5Fui=5Flayout=2063,=20tes?= =?UTF-8?q?t=5Fgui=5Fsmoke=2092,=20test=5Finspector=2049,=20test=5Fplaytes?= =?UTF-8?q?t=2011,=20test=5Fobject=5Fmirror=2074,=20test=5Fui=5Fscale=20OK?= =?UTF-8?q?=20=E2=80=94=20alle=20unveraendert=20gruen.=20Renderproben=20un?= =?UTF-8?q?ter=20/opt/data/scopedvr/work/u4=5Fassets=5Fbeweise/.=20test=5F?= =?UTF-8?q?playtest=5Fspawn=20ist=20VOR=20dieser=20Karte=20rot=20(U1-Regre?= =?UTF-8?q?ssion=20=5Ftest=5Fstart=5Freload,=20siehe=20Kommentar=20auf=20t?= =?UTF-8?q?=5Feaccbfed)=20=E2=80=94=20nicht=20von=20U4=20beruehrt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/panels/assets_panel.gd | 513 ++++++++++++++++++++++++++++++--- tests/test_assets_panel.gd | 322 +++++++++++++++++++++ tests/test_assets_panel.gd.uid | 1 + 3 files changed, 788 insertions(+), 48 deletions(-) create mode 100644 tests/test_assets_panel.gd create mode 100644 tests/test_assets_panel.gd.uid diff --git a/src/ui/panels/assets_panel.gd b/src/ui/panels/assets_panel.gd index 359e040..ff73a3e 100644 --- a/src/ui/panels/assets_panel.gd +++ b/src/ui/panels/assets_panel.gd @@ -1,13 +1,30 @@ extends "res://src/ui/panels/editor_panel.gd" -## Objekt-Bibliothek / Assets-Bereich (aus main.gd ausgezogen, Karte t_eaccbfed -## U1 — Logik unverändert, Karten t_3a998ae4/t_59fc0f21): Suche, Klapp-Gruppen, -## Kacheln. Der Bereich wohnt jetzt UNTEN unter der 3D-Ansicht (Unity- -## Projekt-Fenster-Platz); Klick verhält sich noch wie vorher (U5 ändert das). +## Objekt-Bibliothek / Assets-Bereich (aus main.gd ausgezogen, Karte t_eaccbfed; +## Unity-artiger Ausbau Karte t_69687afc/U4): Kategorien-Leiste, Suche, Zuletzt +## benutzt, Vorschau-Kacheln mit Name + ID, Hover vergrößert die ID, fehlende +## Modelle werden markiert (nicht stillschweigend), Klick wählt aus, +## Doppelklick startet das Platzier-Werkzeug wie bisher. +## Der Bereich wohnt UNTEN unter der 3D-Ansicht (Unity-Projekt-Fenster-Platz). +## +## Kompatibilität (main.gd bindet diese Felder — NICHT umbenennen): +## library_search, library_buttons, _library_group_rows, +## library_open_all_btn, library_close_all_btn, +## _on_library_pressed(), _apply_library_filter(), +## _on_library_expand_all(), _on_library_collapse_all(), highlight_library(). +## Die Klapp-Gruppen bleiben als Körper der „Alle"-Ansicht erhalten; die +## Kategorien-Leiste filtert dieselben Zeilen (Mechanik wie die Suche). -const LIBRARY_CFG_SECTION := "library" # Abschnitt in user://scopedvr_editor.cfg +signal object_selected(oid: String) # Klick auf eine Kachel (nur bei Wechsel) +signal object_activated(oid: String) # Doppelklick — Werkzeug-Start (U5 koppelt) + +const LIBRARY_CFG_SECTION := "library" # Abschnitt in user://scopedvr_editor.cfg +const RECENT_CFG_KEY := "zuletzt_benutzt" # selbiger Abschnitt, Liste von IDs +const RECENT_MAX := 12 # gemerkte Objekte (neueste zuerst) +const CAT_ALLE := "alle" # Pseudo-Kategorie: alles +const CAT_ZULETZT := "zuletzt" # Pseudo-Kategorie: zuletzt benutzt var library_box: VBoxContainer -var library_buttons: Dictionary = {} # id → Button +var library_buttons: Dictionary = {} # id → Button (Haupt-Grid, kanonisch) ## Bibliothek-Gruppen (t_59fc0f21): Klapp-Zustand + Zeilen für Suche/Alle auf/zu. var library_search: LineEdit var library_open_all_btn: Button @@ -16,6 +33,18 @@ var _library_group_open: Dictionary = {} # Gruppen-Key → auf/zu var _library_group_rows: Array = [] # {key, header, box, ids} var _library_bold_font: Font = null +# --- U4 neu: Kategorien, Zähler, Zuletzt benutzt, Auswahl ------------------- +var library_counter: Label # „N von M sichtbar" +var _cat_chips: HFlowContainer # Kategorien-Leiste +var _cat_buttons: Dictionary = {} # Key → Chip-Button +var _cat_active: String = CAT_ALLE # aktive Kategorie (Chip) +var _recent_box: VBoxContainer # Zuletzt-benutzt-Ansicht (im Scroll) +var _recent_grid: GridContainer +var _recent_tiles: Dictionary = {} # id → Button (Zuletzt-Ansicht, frisch gebaut) +var _recent_ids: Array = [] # Reihenfolge: neueste zuerst +var selected_oid: String = "" # per Klick gewähltes Objekt +var _id_font_normal: int = 0 # ID-Schriftgröße (skaliert), Normalzustand + static func spec() -> Dictionary: return {"titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10} @@ -25,16 +54,15 @@ func panel_title() -> String: return "Objekt-Bibliothek" -## Bau (aus der alten main.gd/_build_left_panel übernommen, Logik identisch): -## Suche → „Alle auf/zu“ → Kachel-Liste (library_box) → _fill_library(). -## Der Bereich wohnt jetzt UNTEN (Registry-Gruppe "unten"); Klick verhält sich -## noch wie vorher (U5 ändert das Verhalten). +## Bau: Überschrift → Suche → Kategorien-Leiste → Alle auf/zu → Zähler → +## Klapp-Gruppen-Körper („Alle") + Zuletzt-benutzt-Ansicht. func build() -> void: add_child(_header_label("Objekt-Bibliothek")) - add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis ESC (Rechte Maustaste = Flug).")) + add_child(_muted_label("Klick = auswählen, Doppelklick = Werkzeug starten. Ohne GLB wird der Platzhalter gesetzt und die Kachel markiert (Rechte Maustaste = Flug).")) # Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit - # Treffern klappen auf, leere Gruppen verschwinden. + # Treffern klappen auf, leere Gruppen verschwinden. Größe ist von + # test_ui_scale gepinnt (36 → 45 bei „Mittel") — nicht ändern. library_search = LineEdit.new() library_search.placeholder_text = "Suchen (Name oder ID)…" library_search.clear_button_enabled = true @@ -43,6 +71,8 @@ func build() -> void: library_search.text_submitted.connect(func(_t: String) -> void: library_search.release_focus()) add_child(library_search) + _build_category_chips() + # „Alle auf" / „Alle zu" (t_59fc0f21): Klapp-Zustand aller Gruppen auf einmal. var group_btns := HBoxContainer.new() group_btns.add_theme_constant_override("separation", UIScale.px(6)) @@ -60,6 +90,14 @@ func build() -> void: group_btns.add_child(library_close_all_btn) add_child(group_btns) + # Zähler „N von M sichtbar" (U4): M = Objekte im aktiven Bereich, N = davon + # durch Suche/Kategorie sichtbar. + library_counter = Label.new() + library_counter.add_theme_font_size_override("font_size", UIScale.f(11)) + library_counter.modulate = Color(1, 1, 1, 0.7) + library_counter.tooltip_text = "Sichtbare Kacheln im aktiven Bereich" + add_child(library_counter) + var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.custom_minimum_size = UIScale.v2(0, 280) @@ -69,9 +107,101 @@ func build() -> void: library_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL library_box.add_theme_constant_override("separation", UIScale.px(4)) scroll.add_child(library_box) + _recent_box = VBoxContainer.new() + _recent_box.visible = false + _recent_box.add_theme_constant_override("separation", UIScale.px(4)) + library_box.add_child(_recent_box) _fill_library() + _recent_ids = _recent_load() + _update_recent_chip() + _apply_library_filter(library_search.text) +# ------------------------------------------------------------ Kategorien ---- + +## Kategorien-Leiste (U4): „Alle" + eine Chip je Bibliotheks-Gruppe +## (LibraryBrowser.groups_in_order — KEINE eigene Kategorieliste) + +## „Zuletzt benutzt". Chips zeigen Name + Anzahl; Summe der Gruppen-Chips = +## Gesamtzahl (test_assets_panel rechnet nach). +func _build_category_chips() -> void: + _cat_chips = HFlowContainer.new() + _cat_chips.add_theme_constant_override("h_separation", UIScale.px(4)) + _cat_chips.add_theme_constant_override("v_separation", UIScale.px(4)) + add_child(_cat_chips) + var bg := ButtonGroup.new() + var groups := LibraryBrowser.groups_in_order(main.manifest) + _cat_add_chip(CAT_ALLE, "Alle", _manifest_count(), bg) + for group in groups: + var g_key: String = group["key"] + var g_ids: Array = group["ids"] + if g_ids.is_empty(): + continue + _cat_add_chip(g_key, str(group["label"]), g_ids.size(), bg) + _cat_add_chip(CAT_ZULETZT, "Zuletzt benutzt", 0, bg) + (_cat_buttons[CAT_ZULETZT] as Button).visible = false # taucht auf, sobald benutzt + + +func _manifest_count() -> int: + return (main.manifest.get("objects", {}) as Dictionary).size() + + +func _cat_add_chip(key: String, label: String, count: int, bg: ButtonGroup) -> void: + var chip := Button.new() + chip.toggle_mode = true + chip.button_group = bg + chip.button_pressed = key == _cat_active + chip.text = "%s (%d)" % [label, count] if key != CAT_ZULETZT else label + chip.custom_minimum_size = UIScale.v2(0, 28) + chip.tooltip_text = "Zeigt nur die Gruppe „%s“" % label if key != CAT_ALLE and key != CAT_ZULETZT else "Zeigt alle Objekte" if key == CAT_ALLE else "Die zuletzt ausgewählten/benutzten Objekte" + chip.set_meta("cat_key", key) + chip.set_meta("cat_count", count) + chip.toggled.connect(_on_cat_chip_toggled.bind(key)) + _cat_chips.add_child(chip) + _cat_buttons[key] = chip + + +func _on_cat_chip_toggled(on: bool, key: String) -> void: + if not on: + return # Radio-Gruppe: der neue Chip drückt sich selbst an + _select_category(key) + + +## Kategorie wechseln: Zustand merken (kein Config-Zwang — Chips sind +## Ansichtsfilter, der Klapp-Zustand je Gruppe bleibt in der cfg). +func _select_category(key: String) -> void: + if key != CAT_ZULETZT and not _cat_buttons.has(key): + return + _cat_active = key + for chip_key in _cat_buttons: + var chip := _cat_buttons[chip_key] as Button + chip.set_pressed_no_signal(str(chip_key) == _cat_active) + _apply_library_filter(library_search.text) + + +func _update_recent_chip() -> void: + var chip := _cat_buttons.get(CAT_ZULETZT) as Button + if chip == null: + return + chip.visible = not _recent_ids.is_empty() + chip.text = "Zuletzt benutzt (%d)" % _recent_ids.size() + + +## Objekte des aktiven Bereichs (M im Zähler): Alle → Gesamtkatalog, +## Kategorie → deren IDs, Zuletzt → gemerkte Liste. +func _scope_ids() -> Array: + var lib: Dictionary = main.manifest.get("objects", {}) + if _cat_active == CAT_ALLE: + return lib.keys() + if _cat_active == CAT_ZULETZT: + return _recent_ids.duplicate() + for row in _library_group_rows: + if str(row["key"]) == _cat_active: + return (row["ids"] as Array).duplicate() + return [] + + +# -------------------------------------------------------- Klapp-Gruppen ---- + func _fill_library() -> void: library_buttons.clear() _library_group_rows.clear() @@ -81,6 +211,7 @@ func _fill_library() -> void: # - 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). + # U4: der Körper bleibt („Alle“-Ansicht), die Kategorien-Leiste filtert ihn. var first_open: String = "" for group in LibraryBrowser.groups_in_order(main.manifest): var g_key: String = group["key"] @@ -115,7 +246,41 @@ func _fill_library() -> void: 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), grid) - _library_group_rows.append({"key": g_key, "header": header, "box": group_box, "ids": g_ids, "label": g_label, "count": g_ids.size()}) + _library_group_rows.append({"key": g_key, "header": header, "box": group_box, "ids": g_ids, "label": g_label, "count": g_ids.size(), "grid": grid}) + if not resized.is_connected(_on_panel_resized): + resized.connect(_on_panel_resized) + _on_panel_resized() + + +## Unity-Projekt-Fenster-Feeling (U4): die Spaltenzahl passt sich der BREITE +## des unteren Bereichs an (schmaler Dock = wenige Spalten, breiter = viele). +## Kachel-Zielbreite ≈ 180 px (skaliert über ui_scale). +func _on_panel_resized() -> void: + _apply_grid_columns(size.x) + + +func _apply_grid_columns(width: float) -> void: + var cols := clampi(int(width / float(UIScale.px(180))), 1, 12) + for row in _library_group_rows: + ((row["grid"] as GridContainer)).columns = cols + if _recent_grid != null: + _recent_grid.columns = cols + + +## Kompletter Neubau des Bibliotheks-Körpers (Probe/Umbau): alte Zeilen frei +## geben, dann frisch füllen und den Filter anwenden. Die Zuletzt-Ansicht +## (_recent_box) überlebt den Neubau — sie wird nur aus dem Baum genommen und +## wieder eingehängt, damit die Referenz NICHT auf ein freigegebenes Objekt +## zeigt (sonst „previously freed" im nächsten Filterlauf). +func _rebuild_library() -> void: + if _recent_box != null and _recent_box.get_parent() == library_box: + library_box.remove_child(_recent_box) + for c in library_box.get_children(): + library_box.remove_child(c) + (c as Node).free() + if _recent_box != null and _recent_box.get_parent() == null: + library_box.add_child(_recent_box) + _fill_library() _apply_library_filter(library_search.text) @@ -132,12 +297,18 @@ func _on_library_group_toggled(on: bool, g_key: String, g_label: String, count: func _on_library_expand_all() -> void: for row in _library_group_rows: - (row["header"] as Button).button_pressed = true + var header := row["header"] as Button + header.button_pressed = true + # toggled feuert → box sichtbar; Filter danach neu anwenden, damit + # ein aktiver Kategorie-Filter/Suche nicht weggeblasen wird. + _apply_library_filter(library_search.text) func _on_library_collapse_all() -> void: for row in _library_group_rows: - (row["header"] as Button).button_pressed = false + var header := row["header"] as Button + header.button_pressed = false + _apply_library_filter(library_search.text) func _library_header_font() -> Font: @@ -155,6 +326,8 @@ func _library_load_open_states() -> Dictionary: var out := {} if cfg.has_section(LIBRARY_CFG_SECTION): for key in cfg.get_section_keys(LIBRARY_CFG_SECTION): + if str(key) == RECENT_CFG_KEY: + continue out[str(key)] = bool(cfg.get_value(LIBRARY_CFG_SECTION, str(key), true)) return out @@ -166,37 +339,83 @@ func _library_save_open_state(g_key: String, on: bool) -> void: 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. +# ----------------------------------------------------------------- Suche ---- + +## Live-Filter (t_59fc0f21, U4 erweitert): Suche (Name de/en, ID — case- +## insensitive) geht VOR der Kategorie (sucht im ganzen Katalog); ohne Suche +## zeigt die aktive Kategorie nur ihre Gruppe, „Alle" den Klapp-Zustand wie +## gemerkt, „Zuletzt" die Gemerkten. Danach Zähler „N von M sichtbar". 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: + var visible_count := 0 + # Suche gilt immer für den GANZEN Katalog; ohne Suche zählt der aktive Bereich. + var scope: int = _manifest_count() if not q.is_empty() else _scope_ids().size() + _recent_box.visible = false + if searching: + 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 := _library_oid_matches(str(oid), q) + btn.visible = vis + any = any or vis + if vis: + visible_count += 1 + var header := row["header"] as Button + var box := row["box"] as VBoxContainer + header.visible = any # 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: + elif _cat_active == CAT_ZULETZT: + for row in _library_group_rows: + (row["header"] as Button).visible = false + (row["box"] as VBoxContainer).visible = false + for oid in row["ids"]: + var btn := library_buttons.get(str(oid)) as Button + if btn != null: + btn.visible = false + _fill_recent_box() + _recent_box.visible = true + for oid in _recent_ids: + if _recent_tiles.has(str(oid)): + visible_count += 1 + else: + for row in _library_group_rows: + var in_cat: bool = _cat_active == CAT_ALLE or str(row["key"]) == _cat_active + var ids: Array = row["ids"] + var header := row["header"] as Button + var box := row["box"] as VBoxContainer + header.visible = in_cat + if not in_cat: + box.visible = false + for oid in ids: + var btn := library_buttons.get(str(oid)) as Button + if btn != null: + btn.visible = false + continue var is_open := bool(_library_group_open.get(str(row["key"]), false)) + if _cat_active != CAT_ALLE: + # Kategorie-Ansicht zeigt DIESE Gruppe geöffnet (wie die + # Suche ihre Treffer-Gruppen öffnet) — ohne den gemerkten + # Klapp-Zustand zu überschreiben. + is_open = true box.visible = is_open header.text = _library_header_text(is_open, str(row["label"]), int(row["count"])) + for oid in ids: + var btn := library_buttons.get(str(oid)) as Button + if btn != null: + btn.visible = is_open + if is_open: + visible_count += 1 + if library_counter != null: + library_counter.text = "%d von %d sichtbar" % [visible_count, scope] func _library_oid_matches(oid: String, q: String) -> bool: @@ -210,18 +429,35 @@ func _library_oid_matches(oid: String, q: String) -> bool: return false +# ---------------------------------------------------------------- Kacheln ---- + +## Kachel in ein Grid hängen. canonical = Haupt-Grid (landet in +## library_buttons, wird von interaction/_highlight_library gefunden); +## Zuletzt-benutzt-Kacheln sind eigene frische Buttons (kein Dict-Konflikt). func _add_library_button(oid: String, parent: Container) -> void: + var btn := _build_tile(oid) + parent.add_child(btn) + library_buttons[oid] = btn + + +func _build_tile(oid: String) -> Button: var lib: Dictionary = main.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 missing := _tile_is_missing(oid, entry) var btn := Button.new() btn.toggle_mode = true btn.button_pressed = false btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL btn.custom_minimum_size = UIScale.v2(0, 126) btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid] - btn.pressed.connect(_on_library_pressed.bind(oid)) + if missing: + btn.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)" + btn.pressed.connect(_on_tile_pressed.bind(oid)) + btn.gui_input.connect(_on_tile_gui_input.bind(oid)) + btn.mouse_entered.connect(_on_tile_hover.bind(oid, true)) + btn.mouse_exited.connect(_on_tile_hover.bind(oid, false)) var tex := LibraryBrowser.load_thumb(oid) if tex == null: # Kein Thumbnail → Text-Zeile wie früher (Fehlertoleranz, t_3a998ae4). @@ -229,13 +465,21 @@ func _add_library_button(oid: String, parent: Container) -> void: btn.alignment = HORIZONTAL_ALIGNMENT_LEFT btn.custom_minimum_size = UIScale.v2(0, 44) else: - # Kachel (t_59fc0f21): 96-px-Vorschaubild oben, Name klein darunter. + # Kachel (t_59fc0f21 + U4): 96-px-Vorschaubild oben, ID + Name darunter. # Kinder ignorieren die Maus, damit die Klicks am Button ankommen. + # Reihenfolge Bild → ID → Name: der Name bleibt das LETZTE Label + # (test_library_browser_ui liest das letzte Label als Namen). 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", UIScale.px(2)) btn.add_child(tile) + # Vorschaubild: bleibt DIREKTES Kind der Kachel-VBox (test_library_ + # browser_ui sucht es dort) mit gepinnter Min-Größe (test_ui_scale + # sucht THUMB_DISPLAY_PX × Stufenfaktor). Die „Modell fehlt“-Marke + # hängt als Badge-KIND am TextureRect — der Button ist kein Container, + # eine Extra-Zeile unter dem Bild würde unter die Kachelkante rutschen + # und von der nächsten Reihe überdeckt. var tr := TextureRect.new() tr.texture = tex tr.custom_minimum_size = UIScale.v2(LibraryBrowser.THUMB_DISPLAY_PX, LibraryBrowser.THUMB_DISPLAY_PX) @@ -244,6 +488,17 @@ func _add_library_button(oid: String, parent: Container) -> void: tr.size_flags_horizontal = Control.SIZE_SHRINK_CENTER tr.mouse_filter = Control.MOUSE_FILTER_IGNORE tile.add_child(tr) + var id_lbl := Label.new() + id_lbl.text = oid + _id_font_normal = UIScale.f(9) + id_lbl.add_theme_font_size_override("font_size", _id_font_normal) + id_lbl.modulate = Color(1, 1, 1, 0.6) + id_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + id_lbl.clip_text = true + id_lbl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + id_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + id_lbl.set_meta("tile_id_label", true) + tile.add_child(id_lbl) var lbl := Label.new() lbl.text = name_txt lbl.add_theme_font_size_override("font_size", UIScale.f(11)) @@ -252,27 +507,189 @@ func _add_library_button(oid: String, parent: Container) -> void: 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 + if missing: + # Klar sichtbar markieren (U4): Bild abgedunkelt + Warn-Badge + # als Overlay am unteren Bildrand (immer innerhalb der Kachel). + tr.modulate = Color(1, 1, 1, 0.45) + tr.clip_contents = true + var warn := Label.new() + warn.text = "Modell fehlt" + warn.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) + warn.offset_top = -UIScale.px(13) + warn.offset_bottom = -UIScale.px(1) + warn.add_theme_font_size_override("font_size", UIScale.f(9)) + warn.add_theme_color_override("font_color", Color(1, 0.62, 0.3)) + warn.add_theme_color_override("font_outline_color", Color(0, 0, 0)) + warn.add_theme_constant_override("outline_size", UIScale.px(3)) + warn.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + warn.mouse_filter = Control.MOUSE_FILTER_IGNORE + tr.add_child(warn) + return btn -func _on_library_pressed(oid: String) -> void: +## Fehlendes Modell (U4): nur Katalog-Objekte MIT glb-Eintrag können fehlen; +## kuratierte Primitive (ohne glb) bauen ihren Platzhalter lokal — nie „fehlt". +func _tile_is_missing(oid: String, entry: Dictionary) -> bool: + if str(entry.get("glb", "")).is_empty(): + return false + return GlbMeshStore.fehlt(oid) + + +## Hover (U4): die ID wird GRÖSSER, beim Verlassen wieder normal. +func _on_tile_hover(oid: String, entered: bool) -> void: + _set_tile_hover(oid, entered) + + +func _set_tile_hover(oid: String, on: bool) -> void: + var btn := _tile_button(oid) + if btn == null: + return + var id_lbl := _find_id_label(btn) + if id_lbl == null: + return + id_lbl.add_theme_font_size_override("font_size", UIScale.f(14) if on else _id_font_normal) + + +func _tile_button(oid: String) -> Button: + if library_buttons.has(oid): + return library_buttons[oid] as Button + if _recent_tiles.has(oid): + return _recent_tiles[oid] as Button + return null + + +func _find_id_label(btn: Button) -> Label: + var stack: Array = [btn] + while not stack.is_empty(): + var node: Node = stack.pop_back() + if node is Label and (node as Label).has_meta("tile_id_label"): + return node as Label + for c in node.get_children(): + stack.push_back(c) + return null + + +# --------------------------------------------------- Klick / Doppelklick ---- + +## Klick = auswählen (U4). Das Platzieren selbst startet NICHT mehr hier — +## das übernimmt der Doppelklick (bzw. U5 das Einmal-Platzieren). +func _on_tile_pressed(oid: String) -> void: + _select_tile(oid) + + +## Doppelklick = „gesetzt" haken: Werkzeug startet wie bisher (U4-Vertrag; +## das Einmal-Platzieren kommt in U5). +func _on_tile_gui_input(event: InputEvent, oid: String) -> void: + if event is InputEventMouseButton and event.pressed \ + and event.button_index == MOUSE_BUTTON_LEFT and event.double_click: + _activate_tile(oid) + + +func _select_tile(oid: String) -> void: + var changed := selected_oid != oid + selected_oid = oid + _apply_tile_selection() + _record_recent(oid) + if changed: + object_selected.emit(oid) + + +## Doppelklick-Aktion: Werkzeug-Start 1:1 wie der alte Bibliotheks-Klick +## (interaction.start_placement, Auto-Rückwechsel auf Objekt-Werkzeug). +func _activate_tile(oid: String) -> void: + _select_tile(oid) if str(main.get("tool_mode")) != "objects": main.call("_set_tool_mode", "objects") # Bibliothek-Klick wechselt automatisch zurück if main.interaction.placing and main.interaction.place_oid == oid: - # Nochmal geklickt → Modus beenden (An/Aus ohne Umwege). + # Nochmal (Doppel-)geklickt → Modus beenden (An/Aus ohne Umwege). main.interaction.cancel_placement() main.call("_set_status", "Platzieren abgebrochen.") return - for other_oid in library_buttons: - (library_buttons[other_oid] as Button).button_pressed = false - library_buttons[oid].button_pressed = true main.interaction.start_placement(oid) + object_activated.emit(oid) -## Der Bibliothek-Button des aktiven Platzier-Objekts leuchtet auf. -func highlight_library(oid: String) -> void: +## Auswahll-Marke je Kachel: ausgewählt = voll hell + Rahmen-Akzent. +func _apply_tile_selection() -> void: for other_oid in library_buttons: var btn := library_buttons[other_oid] as Button - btn.button_pressed = other_oid == oid - btn.modulate = Color(1, 1, 1) if oid == other_oid else Color(1, 1, 1, 0.85) + btn.button_pressed = str(other_oid) == selected_oid + btn.modulate = Color(1, 1, 1) if str(other_oid) == selected_oid else Color(1, 1, 1, 0.85) + for recent_oid in _recent_tiles: + var rbtn := _recent_tiles[recent_oid] as Button + rbtn.button_pressed = str(recent_oid) == selected_oid + rbtn.modulate = Color(1, 1, 1) if str(recent_oid) == selected_oid else Color(1, 1, 1, 0.85) + + +## Der Bibliotheks-Button des aktiven Platzier-Objekts leuchtet auf +## (Weitersenden von interaction.gd — nur Anzeige, KEIN Signal). +func highlight_library(oid: String) -> void: + selected_oid = oid + _apply_tile_selection() + if not oid.is_empty(): + _record_recent(oid) + + +# -------------------------------------------------------- Zuletzt benutzt ---- + +func _recent_load() -> Array: + var cfg := ConfigFile.new() + cfg.load("user://scopedvr_editor.cfg") + var raw: Variant = cfg.get_value(LIBRARY_CFG_SECTION, RECENT_CFG_KEY, []) + var lib: Dictionary = main.manifest.get("objects", {}) + var out: Array = [] + for v in (raw as Array): + var oid := str(v) + if lib.has(oid): # nur IDs, die es wirklich gibt + out.append(oid) + return out + + +func _recent_save() -> void: + var cfg := ConfigFile.new() + cfg.load("user://scopedvr_editor.cfg") + cfg.set_value(LIBRARY_CFG_SECTION, RECENT_CFG_KEY, _recent_ids) + cfg.save("user://scopedvr_editor.cfg") + + +## Auswahl/Benutzung merken (neueste zuerst, Ring mit RECENT_MAX Einträgen). +func _record_recent(oid: String) -> void: + if oid.is_empty(): + return + _recent_ids.erase(oid) + _recent_ids.insert(0, oid) + while _recent_ids.size() > RECENT_MAX: + _recent_ids.pop_back() + _recent_save() + _update_recent_chip() + if _cat_active == CAT_ZULETZT: + _fill_recent_box() + + +func _fill_recent_box() -> void: + if _recent_box == null: + return + for c in _recent_box.get_children(): + (c as Node).queue_free() + _recent_tiles.clear() + var head := Label.new() + head.text = "Zuletzt benutzt" + head.add_theme_font_size_override("font_size", UIScale.f(18)) + head.add_theme_font_override("font", _library_header_font()) + _recent_box.add_child(head) + _recent_grid = GridContainer.new() + _recent_grid.columns = 2 + _recent_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _recent_grid.add_theme_constant_override("h_separation", UIScale.px(6)) + _recent_grid.add_theme_constant_override("v_separation", UIScale.px(6)) + _recent_box.add_child(_recent_grid) + var lib: Dictionary = main.manifest.get("objects", {}) + for oid in _recent_ids: + if not lib.has(str(oid)): + continue + var btn := _build_tile(str(oid)) + _recent_grid.add_child(btn) + _recent_tiles[str(oid)] = btn + if str(oid) == selected_oid: + btn.button_pressed = true + btn.modulate = Color(1, 1, 1) diff --git a/tests/test_assets_panel.gd b/tests/test_assets_panel.gd new file mode 100644 index 0000000..885ae45 --- /dev/null +++ b/tests/test_assets_panel.gd @@ -0,0 +1,322 @@ +extends SceneTree +## Editor-Test (headless): Assets-Bereich Unity-artig — Karte t_69687afc (U4). +## Ausführen: godot --headless --path . --script res://tests/test_assets_panel.gd +## Exit 0 = alles grün. +## +## Instanziert die ECHTE Main-UI (Muster tests/test_library_browser_ui.gd) und prüft: +## 1. Kategorien-Leiste: ein Chip je Bibliotheks-Gruppe (keine eigene Liste), +## Summe der Gruppen-Anzahlen = Gesamtzahl des Manifests, „Alle" = Gesamtzahl. +## 2. Suche findet NAME und ID (case-insensitive), Zähler „N von M sichtbar". +## 3. Kategorie-Chip filtert auf genau diese Gruppe (Zähler M = Gruppengröße). +## 4. Fehlendes Modell (glb-Eintrag, keine Quelle) → Kachel trägt die +## Markierung „Modell fehlt" + Tooltip-Hinweis; Primitive ohne glb nie. +## 5. Auswahl-Signal: Klick (Selection) löst object_selected GENAU EINMAL je +## NEUER Auswahl aus (Wiederholung derselben Kachel kein zweites Signal). +## 6. Hover vergrößert die ID-Schrift auf der Kachel. +## 7. Doppelklick-Aktion startet das Platzier-Werkzeug wie bisher und nochmal +## auf derselben Kachel bricht ab (alter An/Aus-Vertrag). +## 8. „Zuletzt benutzt" wird je Auswahl in user://scopedvr_editor.cfg +## gemerkt, überlebt Neu-Instanz und taucht als Chip + Ansicht auf. + +const GLB_QUELLEN_SETTING := "scopedvr/editor/glb_quellen" + +var failures := 0 +var checks := 0 +var main: Control +var assets # src/ui/panels/assets_panel.gd +var selected_count := 0 +var activated_count := 0 + + +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 + assets = main.assets_mod + _check(assets != null, "Assets-Panel über Registry angemeldet") + if assets == null: + _finish() + return + assets.object_selected.connect(_on_selected) + assets.object_activated.connect(_on_activated) + + _test_kategorien() + _test_suche_und_zaehler() + _test_kategorie_filter() + _test_fehlendes_modell() + _test_auswahl_signal() + _test_hover() + _test_doppelklick() + await _test_zuletzt_persistenz() + + _finish() + + +func _on_selected(_oid: String) -> void: + selected_count += 1 + + +func _on_activated(_oid: String) -> void: + activated_count += 1 + + +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]/[fenster] 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") + + +# ------------------------------------------------------------- 1 Kategorien ---- + +func _test_kategorien() -> void: + print("Testgruppe: Kategorien-Leiste (aus der Bibliothek, Summe = Gesamtzahl)") + var lib: Dictionary = main.manifest.get("objects", {}) + var groups := LibraryBrowser.groups_in_order(main.manifest) + var chip_keys: Array = assets._cat_buttons.keys() + _check(chip_keys.has("alle"), "Chip „Alle“ existiert") + _check(chip_keys.has("zuletzt"), "Chip „Zuletzt benutzt“ angelegt (noch versteckt)") + var alle_chip: Button = assets._cat_buttons.get("alle") + _check(int(alle_chip.get_meta("cat_count")) == lib.size(), + "„Alle“ zeigt Gesamtzahl (%d)" % int(alle_chip.get_meta("cat_count"))) + var sum := 0 + var group_keys := {} + for group in groups: + if (group["ids"] as Array).is_empty(): + continue + group_keys[str(group["key"])] = true + var chip: Button = assets._cat_buttons.get(str(group["key"])) + _check(chip != null, "Chip für Gruppe „%s“ existiert" % str(group["label"])) + if chip == null: + continue + sum += int(chip.get_meta("cat_count")) + _check(chip.text.contains(str(group["label"])), "Chip-Text trägt Gruppenname (%s)" % chip.text) + _check(chip_keys.size() == group_keys.size() + 2, "keine erfundenen Kategorien (%d Chips für %d Gruppen + Alle/Zuletzt)" % [chip_keys.size(), group_keys.size()]) + _check(sum == lib.size(), "Summe der Gruppen-Anzahlen = Gesamtzahl (%d = %d)" % [sum, lib.size()]) + _check(alle_chip.button_pressed, "„Alle“ ist Start-Kategorie") + _check(not (assets._cat_buttons.get("zuletzt") as Button).visible, "Zuletzt-Chip startet versteckt (noch nichts benutzt)") + + +# ---------------------------------------------------- 2 Suche + Zähler ---- + +func _zaehler_n() -> int: + var text: String = assets.library_counter.text + var vor: String = text.split(" ")[0] + return int(vor) + + +func _zaehler_m() -> int: + var text: String = assets.library_counter.text + var teile: PackedStringArray = text.split(" ") + return int(teile[2]) + + +func _test_suche_und_zaehler() -> void: + print("Testgruppe: Suche (Name + ID) und Zähler „N von M sichtbar“") + var lib_size := (main.manifest.get("objects", {}) as Dictionary).size() + # Namens-Suche (deutsch, case egal): + main.library_search.text = "PILZ" + assets._apply_library_filter("PILZ") + var pilze := 0 + for oid in (main.manifest.get("objects", {}) as Dictionary).keys(): + var btn := main.library_buttons.get(str(oid)) as Button + if btn != null and btn.visible: + pilze += 1 + _check(pilze == 6, "„PILZ“ (Name, groß) findet 6 Kacheln (ist %d)" % pilze) + _check(assets.library_counter.text == "%d von %d sichtbar" % [6, lib_size], + "Zähler bei Suche: „%s“" % assets.library_counter.text) + # ID-Suche (Kurz-ID): + main.library_search.text = "" + assets._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") + main.library_search.text = "" + assets._apply_library_filter("") + _check(assets.library_counter.text == "%d von %d sichtbar" % [assets._library_group_rows[0]["count"], lib_size], + "Zähler bei „Alle“ (1 Gruppe offen): „%s“" % assets.library_counter.text) + + +# -------------------------------------------------- 3 Kategorie-Filter ---- + +func _test_kategorie_filter() -> void: + print("Testgruppe: Kategorie-Chip filtert auf die Gruppe") + assets._select_category("pilze") + var sichtbar := 0 + for oid in (main.manifest.get("objects", {}) as Dictionary).keys(): + var btn := main.library_buttons.get(str(oid)) as Button + if btn != null and btn.visible: + sichtbar += 1 + _check(sichtbar == 6, "Kategorie „Pilze“: 6 Kacheln sichtbar (ist %d)" % sichtbar) + _check(assets.library_counter.text == "6 von 6 sichtbar", "Zähler in Kategorie: „%s“" % assets.library_counter.text) + var pilze_chip: Button = assets._cat_buttons.get("pilze") + _check(pilze_chip.button_pressed, "Pilze-Chip gedrückt") + assets._select_category("alle") + _check((assets._cat_buttons.get("alle") as Button).button_pressed, "Zurück auf „Alle“") + + +# -------------------------------------------------- 4 Fehlendes Modell ---- + +func _test_fehlendes_modell() -> void: + print("Testgruppe: Fehlendes Modell wird markiert (nicht stillschweigend)") + # Hermetisch: GLB-Quellen auf ein garantiert leeres Lager umlenken → + # JEDES Katalog-Objekt mit glb-Eintrag gilt als fehlend, egal wie die + # Umgebung aussieht. (Leere Liste greift nicht — quellen() fällt auf + # DEFAULT_QUELLEN zurück.) + var alter_cache: Dictionary = GlbMeshStore._cache.duplicate() + GlbMeshStore._cache.clear() + ProjectSettings.set_setting(GLB_QUELLEN_SETTING, ["/nonexistent_glb_lager_u4/assets/objekte"]) + var fehlt_oid := "" + for oid in (main.manifest.get("objects", {}) as Dictionary).keys(): + var entry: Dictionary = (main.manifest.get("objects", {}) as Dictionary).get(oid, {}) + if not str(entry.get("glb", "")).is_empty(): + fehlt_oid = str(oid) + break + _check(not fehlt_oid.is_empty(), "Katalog hat glb-Objekte (Probe: %s)" % fehlt_oid) + if fehlt_oid.is_empty(): + ProjectSettings.set_setting(GLB_QUELLEN_SETTING, GlbMeshStore.DEFAULT_QUELLEN) + return + _check(GlbMeshStore.fehlt(fehlt_oid), "GlbMeshStore.fehlt ohne Lager = true (%s)" % fehlt_oid) + _check(assets._tile_is_missing(fehlt_oid, main.manifest.get("objects", {}).get(fehlt_oid, {})), + "_tile_is_missing(%s) = true ohne Quellen" % fehlt_oid) + _check(not assets._tile_is_missing("crate_wood", main.manifest.get("objects", {}).get("crate_wood", {})), + "Primitive ohne glb-Eintrag gelten NIE als fehlend (crate_wood)") + var probe_grid := GridContainer.new() + root.add_child(probe_grid) + var tile: Button = assets._build_tile(fehlt_oid) + probe_grid.add_child(tile) + var warn: Label = null + var dim_thumb := false + var stack: Array = [tile] + while not stack.is_empty(): + var node: Node = stack.pop_back() + if node is Label and (node as Label).text == "Modell fehlt": + warn = node + if node is TextureRect and (node as TextureRect).modulate.a < 0.9: + dim_thumb = true + for c in node.get_children(): + stack.push_back(c) + _check(warn != null, "Kachel trägt Warn-Label „Modell fehlt“") + _check(dim_thumb, "Vorschaubild bei fehlendem Modell abgedunkelt") + _check(tile.tooltip_text.contains("MODELL FEHLT"), "Tooltip nennt das fehlende Modell") + probe_grid.queue_free() + ProjectSettings.set_setting(GLB_QUELLEN_SETTING, GlbMeshStore.DEFAULT_QUELLEN) + GlbMeshStore._cache = alter_cache + + +# ------------------------------------------------------- 5 Auswahl-Signal ---- + +func _test_auswahl_signal() -> void: + print("Testgruppe: Auswahl-Signal genau einmal je neuer Auswahl") + selected_count = 0 + assets._select_tile("crate_wood") + _check(assets.selected_oid == "crate_wood", "selected_oid gesetzt") + _check(selected_count == 1, "1. Auswahl → genau 1 Signal (ist %d)" % selected_count) + assets._select_tile("crate_wood") + _check(selected_count == 1, "gleiche Kachel nochmal → KEIN zweites Signal (ist %d)" % selected_count) + assets._select_tile("crate_metal") + _check(selected_count == 2, "Wechsel → 2. Signal (ist %d)" % selected_count) + var sel_btn := main.library_buttons.get("crate_metal") as Button + _check(sel_btn != null and sel_btn.modulate.a > 0.95, "gewählte Kachel voll sichtbar") + var unsel_btn := main.library_buttons.get("crate_wood") as Button + _check(unsel_btn != null and unsel_btn.modulate.a < 0.95, "andere Kachel abgedimmt") + + +# --------------------------------------------------------------- 6 Hover ---- + +func _test_hover() -> void: + print("Testgruppe: Hover zeigt die ID größer") + var btn := main.library_buttons.get("crate_metal") as Button + var id_lbl_normal: int = assets._id_font_normal + assets._set_tile_hover("crate_metal", true) + var id_lbl: Label = assets._find_id_label(btn) + _check(id_lbl != null, "Kachel hat ID-Label") + if id_lbl != null: + var gross: int = id_lbl.get_theme_font_size("font_size") + assets._set_tile_hover("crate_metal", false) + var klein: int = id_lbl.get_theme_font_size("font_size") + _check(gross > klein, "Hover vergrößert die ID (%d > %d)" % [gross, klein]) + _check(klein == id_lbl_normal, "ohne Hover wieder Normalgröße (%d)" % klein) + + +# --------------------------------------------------------- 7 Doppelklick ---- + +func _test_doppelklick() -> void: + print("Testgruppe: Doppelklick startet Werkzeug wie bisher, nochmal = aus") + activated_count = 0 + assets._activate_tile("crate_wood") + _check(main.interaction.placing and main.interaction.place_oid == "crate_wood", + "Doppelklick: Platzier-Werkzeug läuft für crate_wood") + _check(activated_count == 1, "object_activated genau 1× (ist %d)" % activated_count) + assets._activate_tile("crate_wood") + _check(not main.interaction.placing, "nochmal dieselbe Kachel: Platzieren abgebrochen") + assets._activate_tile("crate_metal") + _check(main.interaction.placing and main.interaction.place_oid == "crate_metal", + "Wechsel per Doppelklick auf crate_metal") + main.interaction.cancel_placement() + _check(not main.interaction.placing, "aufgeräumt: kein Platzieren mehr aktiv") + + +# --------------------------------------------------- 8 Zuletzt benutzt ---- + +func _test_zuletzt_persistenz() -> void: + print("Testgruppe: „Zuletzt benutzt“ — cfg + Neu-Instanz") + var cfg := ConfigFile.new() + cfg.load("user://scopedvr_editor.cfg") + var lst: Array = cfg.get_value("library", "zuletzt_benutzt", []) + _check(lst.size() >= 2, "cfg [library] zuletzt_benutzt gefüllt (%d)" % lst.size()) + _check(str(lst[0]) == "crate_metal", "neueste Benutzung zuerst (ist %s)" % str(lst[0])) + var chip := assets._cat_buttons.get("zuletzt") as Button + _check(chip != null and chip.visible, "Zuletzt-Chip jetzt sichtbar") + _check(chip.text.contains("2"), "Zuletzt-Chip zählt Einträge (%s)" % chip.text) + # Ansicht: Kategorie „zuletzt" zeigt nur die Gemerkten. + assets._select_category("zuletzt") + _check(assets._recent_box.visible, "Zuletzt-Ansicht sichtbar") + var tiles := 0 + for oid in assets._recent_ids: + if assets._recent_tiles.has(str(oid)): + tiles += 1 + _check(tiles == 2, "Zuletzt-Ansicht zeigt 2 Kacheln (ist %d)" % tiles) + # Neu-Instanz = „Neustart": Chip + Liste kommen zurück. + 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 + assets = main.assets_mod + var chip2 := assets._cat_buttons.get("zuletzt") as Button + _check(chip2 != null and chip2.visible, "nach Neu-Instanz: Zuletzt-Chip sichtbar") + _check(assets._recent_ids.size() == 2, "nach Neu-Instanz: 2 gemerkte IDs (ist %d)" % assets._recent_ids.size()) + _check(str(assets._recent_ids[0]) == "crate_metal", "Reihenfolge hält den Neustart") diff --git a/tests/test_assets_panel.gd.uid b/tests/test_assets_panel.gd.uid new file mode 100644 index 0000000..e0f071e --- /dev/null +++ b/tests/test_assets_panel.gd.uid @@ -0,0 +1 @@ +uid://d3454ajunh2tp