scopedvr-map-core/ui/panels/assets_panel.gd
2026-11-01 19:31:33 +01:00

782 lines
31 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends "res://core/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 _drag_oid := "" # Kachel, die gerade gezogen wird
var _drag_started_placement := false # Ziehen hat das Platzieren gestartet
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"))
# 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, 320)
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(200))), 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, 188)
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))
# Ziehen & Loslassen (25.10.): Kachel anklicken, gedrückt halten, in die Szene
# ziehen — beim Loslassen wird das Objekt dort gesetzt (Doppelklick geht weiter).
btn.set_drag_forwarding(_tile_drag_data.bind(oid, btn), Callable(), Callable())
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.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)
## Ziehen-Start einer Kachel: Vorschau-Etikett an der Maus, Daten = Objekt-ID.
func _tile_drag_data(_at_position: Vector2, oid: String, btn: Button) -> Variant:
if main.doc.is_empty():
return null
_select_tile(oid)
_drag_oid = oid
var prev := PanelContainer.new()
var box := HBoxContainer.new()
prev.add_child(box)
var tex := LibraryBrowser.load_thumb(oid)
if tex != null:
var tr := TextureRect.new()
tr.texture = tex
tr.custom_minimum_size = UIScale.v2(64, 64)
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
box.add_child(tr)
var lbl := Label.new()
lbl.text = LibraryBrowser.entry_label(oid, (main.manifest.get("objects", {}) as Dictionary).get(oid, {}))
box.add_child(lbl)
prev.modulate.a = 0.85
btn.set_drag_preview(prev) # gehört in den Drag-Start: gui_is_dragging() ist hier noch false
return {"svr_oid": oid}
## Maus mit gezogener Kachel ÜBER der 3D-Ansicht: Platzier-Vorschau folgt der Maus.
func drag_hover(oid: String, viewport_pos: Vector2) -> void:
if str(main.get("tool_mode")) != "objects":
main.call("_set_tool_mode", "objects")
if not (main.interaction.placing and main.interaction.place_oid == oid):
main.interaction.start_placement(oid)
_drag_started_placement = true
main.interaction._ghost_update(viewport_pos)
## Loslassen über der 3D-Ansicht: genau wie ein Setz-Klick.
func drag_drop(oid: String, viewport_pos: Vector2) -> void:
drag_hover(oid, viewport_pos)
main.interaction._place_object(viewport_pos, false)
if main.interaction.placing and not BauKatalog.ist_bau(oid):
main.interaction.cancel_placement() # daneben gezielt: Vorschau nicht hängen lassen
_drag_started_placement = false
_drag_oid = ""
func _notification(what: int) -> void:
if what != NOTIFICATION_DRAG_END or _drag_oid.is_empty():
return
# Losgelassen ÜBER der Szene → drag_drop hat aufgeräumt. Sonst (abgebrochen,
# anderswo losgelassen) die Vorschau wieder wegnehmen.
if _drag_started_placement and main != null and main.interaction.placing:
main.interaction.cancel_placement()
_drag_started_placement = false
_drag_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