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() 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