U4 t_69687afc: Assets-Bereich Unity-artig ausgebaut — Kategorien-Leiste (Alle + Gruppen aus LibraryBrowser, Summe = Gesamtzahl), Suche Name+ID, Zaehler "N von M sichtbar", Zuletzt benutzt (cfg [library] zuletzt_benutzt, Chip + Ansicht), Kacheln mit 96-px-Vorschau + ID + Name, Hover vergroessert die ID, fehlende GLB = abgedunkeltes Bild + orange Badge "Modell fehlt" (GlbMeshStore.fehlt), Klick = auswaehlen (Signal object_selected genau 1x je Wechsel), Doppelklick = Werkzeug starten wie bisher (nochmal = abbrechen), Spaltenzahl breitenabhaengig (180 px je Kachel, ueber ui_scale), Dock-Groesse bleibt in [fenster] gemerkt (U1). Klapp-Gruppen + Alle auf/zu + gepinnte Masse (Suchfeld 36, Kachel 126, Thumb 96) unveraendert. Suite tests/test_assets_panel.gd 75 checks ALL OK; test_library_browser_ui 30, test_library 101, test_ui_layout 63, test_gui_smoke 92, test_inspector 49, test_playtest 11, test_object_mirror 74, test_ui_scale OK — alle unveraendert gruen. Renderproben unter /opt/data/scopedvr/work/u4_assets_beweise/. test_playtest_spawn ist VOR dieser Karte rot (U1-Regression _test_start_reload, siehe Kommentar auf t_eaccbfed) — nicht von U4 beruehrt.
This commit is contained in:
parent
69ffe23a71
commit
8f0e011a07
3 changed files with 788 additions and 48 deletions
|
|
@ -1,13 +1,30 @@
|
||||||
extends "res://src/ui/panels/editor_panel.gd"
|
extends "res://src/ui/panels/editor_panel.gd"
|
||||||
## Objekt-Bibliothek / Assets-Bereich (aus main.gd ausgezogen, Karte t_eaccbfed
|
## Objekt-Bibliothek / Assets-Bereich (aus main.gd ausgezogen, Karte t_eaccbfed;
|
||||||
## U1 — Logik unverändert, Karten t_3a998ae4/t_59fc0f21): Suche, Klapp-Gruppen,
|
## Unity-artiger Ausbau Karte t_69687afc/U4): Kategorien-Leiste, Suche, Zuletzt
|
||||||
## Kacheln. Der Bereich wohnt jetzt UNTEN unter der 3D-Ansicht (Unity-
|
## benutzt, Vorschau-Kacheln mit Name + ID, Hover vergrößert die ID, fehlende
|
||||||
## Projekt-Fenster-Platz); Klick verhält sich noch wie vorher (U5 ändert das).
|
## 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).
|
||||||
|
|
||||||
|
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 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_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.
|
## Bibliothek-Gruppen (t_59fc0f21): Klapp-Zustand + Zeilen für Suche/Alle auf/zu.
|
||||||
var library_search: LineEdit
|
var library_search: LineEdit
|
||||||
var library_open_all_btn: Button
|
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_group_rows: Array = [] # {key, header, box, ids}
|
||||||
var _library_bold_font: Font = null
|
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:
|
static func spec() -> Dictionary:
|
||||||
return {"titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10}
|
return {"titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10}
|
||||||
|
|
@ -25,16 +54,15 @@ func panel_title() -> String:
|
||||||
return "Objekt-Bibliothek"
|
return "Objekt-Bibliothek"
|
||||||
|
|
||||||
|
|
||||||
## Bau (aus der alten main.gd/_build_left_panel übernommen, Logik identisch):
|
## Bau: Überschrift → Suche → Kategorien-Leiste → Alle auf/zu → Zähler →
|
||||||
## Suche → „Alle auf/zu“ → Kachel-Liste (library_box) → _fill_library().
|
## Klapp-Gruppen-Körper („Alle") + Zuletzt-benutzt-Ansicht.
|
||||||
## Der Bereich wohnt jetzt UNTEN (Registry-Gruppe "unten"); Klick verhält sich
|
|
||||||
## noch wie vorher (U5 ändert das Verhalten).
|
|
||||||
func build() -> void:
|
func build() -> void:
|
||||||
add_child(_header_label("Objekt-Bibliothek"))
|
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
|
# 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 = LineEdit.new()
|
||||||
library_search.placeholder_text = "Suchen (Name oder ID)…"
|
library_search.placeholder_text = "Suchen (Name oder ID)…"
|
||||||
library_search.clear_button_enabled = true
|
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())
|
library_search.text_submitted.connect(func(_t: String) -> void: library_search.release_focus())
|
||||||
add_child(library_search)
|
add_child(library_search)
|
||||||
|
|
||||||
|
_build_category_chips()
|
||||||
|
|
||||||
# „Alle auf" / „Alle zu" (t_59fc0f21): Klapp-Zustand aller Gruppen auf einmal.
|
# „Alle auf" / „Alle zu" (t_59fc0f21): Klapp-Zustand aller Gruppen auf einmal.
|
||||||
var group_btns := HBoxContainer.new()
|
var group_btns := HBoxContainer.new()
|
||||||
group_btns.add_theme_constant_override("separation", UIScale.px(6))
|
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)
|
group_btns.add_child(library_close_all_btn)
|
||||||
add_child(group_btns)
|
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()
|
var scroll := ScrollContainer.new()
|
||||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
scroll.custom_minimum_size = UIScale.v2(0, 280)
|
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.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
library_box.add_theme_constant_override("separation", UIScale.px(4))
|
library_box.add_theme_constant_override("separation", UIScale.px(4))
|
||||||
scroll.add_child(library_box)
|
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()
|
_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:
|
func _fill_library() -> void:
|
||||||
library_buttons.clear()
|
library_buttons.clear()
|
||||||
_library_group_rows.clear()
|
_library_group_rows.clear()
|
||||||
|
|
@ -81,6 +211,7 @@ func _fill_library() -> void:
|
||||||
# - STANDARD EINGEKLAPPT — nur die erste Gruppe ist offen (Übersicht!),
|
# - STANDARD EINGEKLAPPT — nur die erste Gruppe ist offen (Übersicht!),
|
||||||
# - Klapp-Zustand je Gruppe wird in user://scopedvr_editor.cfg gemerkt,
|
# - Klapp-Zustand je Gruppe wird in user://scopedvr_editor.cfg gemerkt,
|
||||||
# - Objekte als Kacheln mit 96-px-Vorschaubild (2 Spalten je Gruppe).
|
# - 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 = ""
|
var first_open: String = ""
|
||||||
for group in LibraryBrowser.groups_in_order(main.manifest):
|
for group in LibraryBrowser.groups_in_order(main.manifest):
|
||||||
var g_key: String = group["key"]
|
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))
|
header.toggled.connect(_on_library_group_toggled.bind(g_key, g_label, g_ids.size(), header, group_box))
|
||||||
for oid in g_ids:
|
for oid in g_ids:
|
||||||
_add_library_button(str(oid), grid)
|
_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)
|
_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:
|
func _on_library_expand_all() -> void:
|
||||||
for row in _library_group_rows:
|
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:
|
func _on_library_collapse_all() -> void:
|
||||||
for row in _library_group_rows:
|
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:
|
func _library_header_font() -> Font:
|
||||||
|
|
@ -155,6 +326,8 @@ func _library_load_open_states() -> Dictionary:
|
||||||
var out := {}
|
var out := {}
|
||||||
if cfg.has_section(LIBRARY_CFG_SECTION):
|
if cfg.has_section(LIBRARY_CFG_SECTION):
|
||||||
for key in cfg.get_section_keys(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))
|
out[str(key)] = bool(cfg.get_value(LIBRARY_CFG_SECTION, str(key), true))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
@ -166,12 +339,20 @@ func _library_save_open_state(g_key: String, on: bool) -> void:
|
||||||
cfg.save("user://scopedvr_editor.cfg")
|
cfg.save("user://scopedvr_editor.cfg")
|
||||||
|
|
||||||
|
|
||||||
## Live-Suche (t_59fc0f21): leer = alles normal sichtbar (Klapp-Zustand wie
|
# ----------------------------------------------------------------- Suche ----
|
||||||
## gemerkt); Text = nur Treffer (Name de/en, ID — case-insensitive), Gruppen
|
|
||||||
## mit Treffern bleiben offen, leere verschwinden.
|
## 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:
|
func _apply_library_filter(query: String) -> void:
|
||||||
var q := query.strip_edges().to_lower()
|
var q := query.strip_edges().to_lower()
|
||||||
var searching := not q.is_empty()
|
var searching := not q.is_empty()
|
||||||
|
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:
|
for row in _library_group_rows:
|
||||||
var ids: Array = row["ids"]
|
var ids: Array = row["ids"]
|
||||||
var any := false
|
var any := false
|
||||||
|
|
@ -179,24 +360,62 @@ func _apply_library_filter(query: String) -> void:
|
||||||
var btn := library_buttons.get(str(oid)) as Button
|
var btn := library_buttons.get(str(oid)) as Button
|
||||||
if btn == null:
|
if btn == null:
|
||||||
continue
|
continue
|
||||||
var vis := true
|
var vis := _library_oid_matches(str(oid), q)
|
||||||
if searching:
|
|
||||||
vis = _library_oid_matches(str(oid), q)
|
|
||||||
btn.visible = vis
|
btn.visible = vis
|
||||||
any = any or vis
|
any = any or vis
|
||||||
|
if vis:
|
||||||
|
visible_count += 1
|
||||||
var header := row["header"] as Button
|
var header := row["header"] as Button
|
||||||
var box := row["box"] as VBoxContainer
|
var box := row["box"] as VBoxContainer
|
||||||
header.visible = any
|
header.visible = any
|
||||||
if searching:
|
|
||||||
# Treffer-Gruppen bleiben auf — auch wenn sie zugeklappt waren
|
# Treffer-Gruppen bleiben auf — auch wenn sie zugeklappt waren
|
||||||
# (Pfeil zeigt dann offen, kosmetisch konsistent).
|
# (Pfeil zeigt dann offen, kosmetisch konsistent).
|
||||||
box.visible = any
|
box.visible = any
|
||||||
if any:
|
if any:
|
||||||
header.text = _library_header_text(true, str(row["label"]), int(row["count"]))
|
header.text = _library_header_text(true, str(row["label"]), int(row["count"]))
|
||||||
|
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:
|
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))
|
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
|
box.visible = is_open
|
||||||
header.text = _library_header_text(is_open, str(row["label"]), int(row["count"]))
|
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:
|
func _library_oid_matches(oid: String, q: String) -> bool:
|
||||||
|
|
@ -210,18 +429,35 @@ func _library_oid_matches(oid: String, q: String) -> bool:
|
||||||
return false
|
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:
|
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 lib: Dictionary = main.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 name_txt := LibraryBrowser.entry_label(oid, entry)
|
||||||
|
var missing := _tile_is_missing(oid, entry)
|
||||||
var btn := Button.new()
|
var btn := Button.new()
|
||||||
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.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
btn.custom_minimum_size = UIScale.v2(0, 126)
|
btn.custom_minimum_size = UIScale.v2(0, 126)
|
||||||
btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid]
|
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)
|
var tex := LibraryBrowser.load_thumb(oid)
|
||||||
if tex == null:
|
if tex == null:
|
||||||
# Kein Thumbnail → Text-Zeile wie früher (Fehlertoleranz, t_3a998ae4).
|
# 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.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
btn.custom_minimum_size = UIScale.v2(0, 44)
|
btn.custom_minimum_size = UIScale.v2(0, 44)
|
||||||
else:
|
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.
|
# 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()
|
var tile := VBoxContainer.new()
|
||||||
tile.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
tile.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
tile.set_anchors_preset(Control.PRESET_FULL_RECT)
|
tile.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
tile.add_theme_constant_override("separation", UIScale.px(2))
|
tile.add_theme_constant_override("separation", UIScale.px(2))
|
||||||
btn.add_child(tile)
|
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()
|
var tr := TextureRect.new()
|
||||||
tr.texture = tex
|
tr.texture = tex
|
||||||
tr.custom_minimum_size = UIScale.v2(LibraryBrowser.THUMB_DISPLAY_PX, LibraryBrowser.THUMB_DISPLAY_PX)
|
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.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
tile.add_child(tr)
|
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()
|
var lbl := Label.new()
|
||||||
lbl.text = name_txt
|
lbl.text = name_txt
|
||||||
lbl.add_theme_font_size_override("font_size", UIScale.f(11))
|
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.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
tile.add_child(lbl)
|
tile.add_child(lbl)
|
||||||
parent.add_child(btn)
|
if missing:
|
||||||
library_buttons[oid] = btn
|
# 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":
|
if str(main.get("tool_mode")) != "objects":
|
||||||
main.call("_set_tool_mode", "objects") # Bibliothek-Klick wechselt automatisch zurück
|
main.call("_set_tool_mode", "objects") # Bibliothek-Klick wechselt automatisch zurück
|
||||||
if main.interaction.placing and main.interaction.place_oid == oid:
|
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.interaction.cancel_placement()
|
||||||
main.call("_set_status", "Platzieren abgebrochen.")
|
main.call("_set_status", "Platzieren abgebrochen.")
|
||||||
return
|
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)
|
main.interaction.start_placement(oid)
|
||||||
|
object_activated.emit(oid)
|
||||||
|
|
||||||
|
|
||||||
## Der Bibliothek-Button des aktiven Platzier-Objekts leuchtet auf.
|
## Auswahll-Marke je Kachel: ausgewählt = voll hell + Rahmen-Akzent.
|
||||||
func highlight_library(oid: String) -> void:
|
func _apply_tile_selection() -> void:
|
||||||
for other_oid in library_buttons:
|
for other_oid in library_buttons:
|
||||||
var btn := library_buttons[other_oid] as Button
|
var btn := library_buttons[other_oid] as Button
|
||||||
btn.button_pressed = other_oid == oid
|
btn.button_pressed = str(other_oid) == selected_oid
|
||||||
btn.modulate = Color(1, 1, 1) if oid == other_oid else Color(1, 1, 1, 0.85)
|
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)
|
||||||
|
|
|
||||||
322
tests/test_assets_panel.gd
Normal file
322
tests/test_assets_panel.gd
Normal file
|
|
@ -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")
|
||||||
1
tests/test_assets_panel.gd.uid
Normal file
1
tests/test_assets_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d3454ajunh2tp
|
||||||
Loading…
Reference in a new issue