723 lines
29 KiB
GDScript
723 lines
29 KiB
GDScript
extends "res://src/ui/panels/editor_panel.gd"
|
||
## 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).
|
||
|
||
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 (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
|
||
var library_close_all_btn: Button
|
||
var _library_group_open: Dictionary = {} # Gruppen-Key → auf/zu
|
||
var _library_group_rows: Array = [] # {key, header, box, ids}
|
||
var _library_bold_font: Font = null
|
||
|
||
# --- 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
|
||
var _empty_hint: Label # Leerzustand (U7): kein Treffer / leerer Bereich
|
||
|
||
|
||
static func spec() -> Dictionary:
|
||
return {"titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10}
|
||
|
||
|
||
func panel_title() -> String:
|
||
return "Objekt-Bibliothek"
|
||
|
||
|
||
## 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("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. 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
|
||
library_search.custom_minimum_size = UIScale.v2(0, 36)
|
||
library_search.text_changed.connect(_apply_library_filter)
|
||
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(8)) # U7-Raster
|
||
library_open_all_btn = Button.new()
|
||
library_open_all_btn.text = "Alle auf"
|
||
library_open_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
library_open_all_btn.tooltip_text = "Klappt ALLE Gruppen auf"
|
||
library_open_all_btn.pressed.connect(_on_library_expand_all)
|
||
group_btns.add_child(library_open_all_btn)
|
||
library_close_all_btn = Button.new()
|
||
library_close_all_btn.text = "Alle zu"
|
||
library_close_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
library_close_all_btn.tooltip_text = "Klappt ALLE Gruppen zu"
|
||
library_close_all_btn.pressed.connect(_on_library_collapse_all)
|
||
group_btns.add_child(library_close_all_btn)
|
||
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()
|
||
# U7: Nebentext-Größe + Theme-Farbe (Editor/colors/field).
|
||
library_counter.add_theme_font_size_override("font_size", UIScale.small_font_size())
|
||
library_counter.add_theme_color_override("font_color", theme_color("field"))
|
||
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)
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
add_child(scroll)
|
||
library_box = VBoxContainer.new()
|
||
library_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
library_box.add_theme_constant_override("separation", UIScale.px(4))
|
||
scroll.add_child(library_box)
|
||
# Leerzustand (U7, „fertig aussehen"): freundlicher Klartext statt
|
||
# stummer leerer Fläche — Suchbegriff ohne Treffer oder leerer Bereich.
|
||
_empty_hint = Label.new()
|
||
_empty_hint.text = "Nichts gefunden — anderen Suchbegriff oder „Alle“ versuchen."
|
||
_empty_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_empty_hint.add_theme_color_override("font_color", theme_color("muted"))
|
||
_empty_hint.visible = false
|
||
scroll.add_child(_empty_hint)
|
||
_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()
|
||
_library_group_open = _library_load_open_states()
|
||
# Gruppen-Browser (Karte t_3a998ae4, Überarbeitung t_59fc0f21):
|
||
# - Klapp-Überschriften je Gruppe (fett, 18 px, Name + Anzahl),
|
||
# - STANDARD EINGEKLAPPT — nur die erste Gruppe ist offen (Übersicht!),
|
||
# - Klapp-Zustand je Gruppe wird in user://scopedvr_editor.cfg gemerkt,
|
||
# - Objekte als Kacheln mit 96-px-Vorschaubild (2 Spalten je Gruppe).
|
||
# 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"]
|
||
var g_label: String = group["label"]
|
||
var g_ids: Array = group["ids"]
|
||
if g_ids.is_empty():
|
||
continue
|
||
if first_open.is_empty():
|
||
first_open = g_key
|
||
var is_open: bool = _library_group_open.get(g_key, g_key == first_open)
|
||
_library_group_open[g_key] = is_open
|
||
var header := Button.new()
|
||
header.text = _library_header_text(is_open, g_label, g_ids.size())
|
||
header.toggle_mode = true
|
||
header.button_pressed = is_open
|
||
header.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
header.custom_minimum_size = UIScale.v2(0, 34)
|
||
header.tooltip_text = "Gruppe „%s“ auf-/zuklappen" % g_label
|
||
header.add_theme_font_size_override("font_size", UIScale.heading_font_size())
|
||
header.add_theme_font_override("font", _library_header_font())
|
||
library_box.add_child(header)
|
||
var group_box := VBoxContainer.new()
|
||
group_box.add_theme_constant_override("separation", UIScale.px(4))
|
||
group_box.visible = is_open
|
||
var grid := GridContainer.new()
|
||
grid.columns = 2
|
||
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
grid.add_theme_constant_override("h_separation", UIScale.px(8)) # U7-Raster
|
||
grid.add_theme_constant_override("v_separation", UIScale.px(8)) # U7-Raster
|
||
group_box.add_child(grid)
|
||
library_box.add_child(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:
|
||
_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(), "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)
|
||
|
||
|
||
func _library_header_text(on: bool, label: String, count: int) -> String:
|
||
return "%s %s (%d)" % ["▾" if on else "▸", label, count]
|
||
|
||
|
||
func _on_library_group_toggled(on: bool, g_key: String, g_label: String, count: int, header: Button, group_box: VBoxContainer) -> void:
|
||
group_box.visible = on
|
||
header.text = _library_header_text(on, g_label, count)
|
||
_library_group_open[g_key] = on
|
||
_library_save_open_state(g_key, on)
|
||
|
||
|
||
func _on_library_expand_all() -> void:
|
||
for row in _library_group_rows:
|
||
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:
|
||
var header := row["header"] as Button
|
||
header.button_pressed = false
|
||
_apply_library_filter(library_search.text)
|
||
|
||
|
||
func _library_header_font() -> Font:
|
||
if _library_bold_font == null:
|
||
var sf := SystemFont.new()
|
||
sf.font_names = PackedStringArray(["Noto Sans", "DejaVu Sans", "Ubuntu", "sans-serif"])
|
||
sf.font_weight = 700
|
||
_library_bold_font = sf
|
||
return _library_bold_font
|
||
|
||
|
||
func _library_load_open_states() -> Dictionary:
|
||
var cfg := ConfigFile.new()
|
||
cfg.load("user://scopedvr_editor.cfg")
|
||
var out := {}
|
||
if cfg.has_section(LIBRARY_CFG_SECTION):
|
||
for key in cfg.get_section_keys(LIBRARY_CFG_SECTION):
|
||
if str(key) == RECENT_CFG_KEY:
|
||
continue
|
||
# Robustheit (t_efb3d022): cfg-Einträge dieser Sektion müssen Bool-
|
||
# Zustände sein. Ein kaputter Eintrag (String/Array/sonstwas) darf
|
||
# KEINEN SCRIPT ERROR werfen (bool(<Array>) ist kein gültiger
|
||
# Konstruktor) — sonst brechen die Klapp-Zustände komplett weg.
|
||
var val = cfg.get_value(LIBRARY_CFG_SECTION, str(key), true)
|
||
if typeof(val) == TYPE_BOOL:
|
||
out[str(key)] = val
|
||
else:
|
||
out[str(key)] = true
|
||
return out
|
||
|
||
|
||
func _library_save_open_state(g_key: String, on: bool) -> void:
|
||
var cfg := ConfigFile.new()
|
||
cfg.load("user://scopedvr_editor.cfg")
|
||
cfg.set_value(LIBRARY_CFG_SECTION, g_key, on)
|
||
cfg.save("user://scopedvr_editor.cfg")
|
||
|
||
|
||
# ----------------------------------------------------------------- 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()
|
||
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"]))
|
||
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]
|
||
# Leerzustand (U7): 0 Treffer → Klartext-Hinweis zeigen statt stummer Fläche.
|
||
if _empty_hint != null:
|
||
_empty_hint.visible = visible_count == 0
|
||
|
||
|
||
func _library_oid_matches(oid: String, q: String) -> bool:
|
||
if oid.to_lower().contains(q):
|
||
return true
|
||
var entry: Dictionary = (main.manifest.get("objects", {}) as Dictionary).get(oid, {})
|
||
if LibraryBrowser.entry_label(oid, entry).to_lower().contains(q):
|
||
return true
|
||
if LibraryBrowser.object_label(oid).to_lower().contains(q):
|
||
return true
|
||
return false
|
||
|
||
|
||
# ---------------------------------------------------------------- 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]
|
||
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).
|
||
btn.text = "%s · %d Polys" % [name_txt, poly]
|
||
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||
btn.custom_minimum_size = UIScale.v2(0, 44)
|
||
else:
|
||
# 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(4)) # U7-Raster
|
||
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)
|
||
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
tr.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
tile.add_child(tr)
|
||
var id_lbl := Label.new()
|
||
id_lbl.text = oid
|
||
# U7: Tile-ID als Nebentext-Größe, Farbe über Theme (Editor/colors/title).
|
||
_id_font_normal = UIScale.small_font_size()
|
||
id_lbl.add_theme_font_size_override("font_size", _id_font_normal)
|
||
id_lbl.add_theme_color_override("font_color", theme_color("title"))
|
||
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.small_font_size())
|
||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
lbl.clip_text = true
|
||
lbl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
tile.add_child(lbl)
|
||
if missing:
|
||
# Klar sichtbar markieren (U4): Bild abgedunkelt + Warn-Badge
|
||
# als Overlay am unteren Bildrand (immer innerhalb der Kachel).
|
||
# U7: Abdunkeln nur über Alpha (Farbe bleibt weiß), Badge-Farbe
|
||
# über Theme (Editor/colors/warning + badge_outline), Nebentext-
|
||
# Größe statt Mikroschrift (U7-Raster: zwei Größen + Überschrift).
|
||
tr.modulate.a = 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(18)
|
||
warn.offset_bottom = -UIScale.px(1)
|
||
warn.add_theme_font_size_override("font_size", UIScale.small_font_size())
|
||
warn.add_theme_color_override("font_color", theme_color("warning"))
|
||
warn.add_theme_color_override("font_outline_color", theme_color("badge_outline"))
|
||
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
|
||
|
||
|
||
## 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
|
||
# U7-Raster: Hover-Stufe ist die Body-Schrift (klein → body), kein Wert ins Freie.
|
||
id_lbl.add_theme_font_size_override("font_size",
|
||
UIScale.font_size() 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 (Doppel-)geklickt → Modus beenden (An/Aus ohne Umwege).
|
||
main.interaction.cancel_placement()
|
||
main.call("_set_status", "Platzieren abgebrochen.")
|
||
return
|
||
main.interaction.start_placement(oid)
|
||
object_activated.emit(oid)
|
||
|
||
|
||
## 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 = str(other_oid) == selected_oid
|
||
# U7: Auswahl-Markierung nur über Alpha (Farbe bleibt weiß).
|
||
btn.modulate.a = 1.0 if str(other_oid) == selected_oid else 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.a = 1.0 if str(recent_oid) == selected_oid else 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.heading_font_size())
|
||
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(8)) # U7-Raster
|
||
_recent_grid.add_theme_constant_override("v_separation", UIScale.px(8)) # U7-Raster
|
||
_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.a = 1.0
|