From 69ffe23a71982a0e91489b44250662f990fc8883 Mon Sep 17 00:00:00 2001 From: scopedvr-operator Date: Tue, 20 Oct 2026 09:11:00 +0000 Subject: [PATCH] =?UTF-8?q?t=5Feaccbfed:=20U1=20UI-Fundament=20(Operator-P?= =?UTF-8?q?ush)=20=E2=80=94=20main.gd=203677->2479=20Zeilen,=20src/ui/pane?= =?UTF-8?q?ls/=20(11=20Module=20+=20registry),=20Tabs=20rechts+links,=20As?= =?UTF-8?q?sets=20unten,=20theme/editor=5Ftheme.tres,=20tests/test=5Fui=5F?= =?UTF-8?q?layout.gd=20(63=20checks).=20Worker=20meldete=20fertig=20ohne?= =?UTF-8?q?=20Push;=20Arbeit=20aus=20dem=20Scratch-Ordner=20gerettet.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/main.gd | 2024 +++++------------------ src/ui/panels/assets_panel.gd | 278 ++++ src/ui/panels/assets_panel.gd.uid | 1 + src/ui/panels/check_panel.gd | 146 ++ src/ui/panels/check_panel.gd.uid | 1 + src/ui/panels/editor_panel.gd | 53 + src/ui/panels/editor_panel.gd.uid | 1 + src/ui/panels/map_panel.gd | 127 ++ src/ui/panels/map_panel.gd.uid | 1 + src/ui/panels/object_tools_panel.gd | 34 + src/ui/panels/object_tools_panel.gd.uid | 1 + src/ui/panels/registry.gd | 87 + src/ui/panels/registry.gd.uid | 1 + src/ui/panels/scene_tree_panel.gd | 155 ++ src/ui/panels/scene_tree_panel.gd.uid | 1 + src/ui/panels/selection_panel.gd | 234 +++ src/ui/panels/selection_panel.gd.uid | 1 + src/ui/panels/status_bar.gd | 58 + src/ui/panels/status_bar.gd.uid | 1 + src/ui/panels/terrain_panel.gd | 279 ++++ src/ui/panels/terrain_panel.gd.uid | 1 + src/ui/panels/toolbar.gd | 157 ++ src/ui/panels/toolbar.gd.uid | 1 + tests/test_ui_layout.gd | 155 ++ tests/test_ui_layout.gd.uid | 1 + theme/editor_theme.tres | 124 ++ tools/run_suites.py | 29 + 27 files changed, 2341 insertions(+), 1611 deletions(-) create mode 100644 src/ui/panels/assets_panel.gd create mode 100644 src/ui/panels/assets_panel.gd.uid create mode 100644 src/ui/panels/check_panel.gd create mode 100644 src/ui/panels/check_panel.gd.uid create mode 100644 src/ui/panels/editor_panel.gd create mode 100644 src/ui/panels/editor_panel.gd.uid create mode 100644 src/ui/panels/map_panel.gd create mode 100644 src/ui/panels/map_panel.gd.uid create mode 100644 src/ui/panels/object_tools_panel.gd create mode 100644 src/ui/panels/object_tools_panel.gd.uid create mode 100644 src/ui/panels/registry.gd create mode 100644 src/ui/panels/registry.gd.uid create mode 100644 src/ui/panels/scene_tree_panel.gd create mode 100644 src/ui/panels/scene_tree_panel.gd.uid create mode 100644 src/ui/panels/selection_panel.gd create mode 100644 src/ui/panels/selection_panel.gd.uid create mode 100644 src/ui/panels/status_bar.gd create mode 100644 src/ui/panels/status_bar.gd.uid create mode 100644 src/ui/panels/terrain_panel.gd create mode 100644 src/ui/panels/terrain_panel.gd.uid create mode 100644 src/ui/panels/toolbar.gd create mode 100644 src/ui/panels/toolbar.gd.uid create mode 100644 tests/test_ui_layout.gd create mode 100644 tests/test_ui_layout.gd.uid create mode 100644 theme/editor_theme.tres create mode 100644 tools/run_suites.py diff --git a/src/ui/main.gd b/src/ui/main.gd index 92db4d3..1844d0c 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -1,16 +1,30 @@ extends Control -## ScopedVR Map-Editor — Hauptfenster (M2 UX-Fundament, Karte t_40d855d0). +## ScopedVR Map-Editor — Hauptfenster (M2 UX-Fundament, Karte t_40d855d0; +## U1-Modularisierung, Karte t_eaccbfed). ## -## Alles UI wird hier per Code gebaut (eine einzige .tscn mit Root-Knoten). -## Struktur: Top-Bar (Menü + Rückgängig/Wiederholen) | links Objekt-Bibliothek -## | Mitte 3D-Viewport | rechts Map-Eigenschaften + Auswahl | unten Statusleiste. +## U1 (Unity-artiges Layout-Fundament): diese Datei ist jetzt ein DÜNNER +## KOORDINATOR — Dokument/History, Werkzeug-Umschaltung, Shortcuts, Dialoge, +## Update, Playtest. Die Oberflächen-Bereiche wohnen in eigenen Modulen unter +## src/ui/panels/ und melden sich DATENGETRIEBEN in +## src/ui/panels/registry.gd an; das Layout wird ausschließlich aus der +## Registry gebaut (Operator-Vorgabe 20.10. — die Umbau-Karten U2–U7 ändern +## je nur IHR Modul + eine Registry-Zeile, nie wieder main.gd). ## -## UX-Fundament dieser Karte (Chef-Grundsatz Sly — map-editor-README): +## Layout (Schaubild in /opt/data/scopedvr/KARTEN_UI_UNITY_vc27.md): +## Toolbar oben · links Tab "Map-Inhalt" · Mitte 3D-Ansicht · +## rechts Tabs (Inspektor/Auswahl/Map/Terrain/Prüfen) · +## unten Assets (Objekt-Bibliothek) · Statuszeile. +## Alles über HSplit/HSplit/VSplit größenverstellbar; die Splitter-Lage +## bleibt in user://scopedvr_editor.cfg ([fenster] Abschnitt) gemerkt. +## +## Farben/Namen kommen aus theme/editor_theme.tres (einzige Farbquelle). +## +## UX-Fundament (Chef-Grundsatz Sly — map-editor-README): ## 1. Undo/Redo (Strg+Z / Strg+Y) über ActionHistory — spätere Werkzeuge ## (Terrain, Spawn-Zonen) melden sich mit snapshot/commit einfach an. ## 2. Ghost-Preview: Platzier-Vorschau schwebt transparent am Cursor ## (interaction.gd), Klick setzt das Objekt. -## 3. Auswahl-Anfassen: Klick wählt (orange), Ziehen verschiebt, +## 3. Auswahl-Anfassen: Klick wählt, Ziehen verschiebt, ## R ODER Mausrad dreht (15°-Raster), Entf löscht. ## 4. Helfende Meldungen: Klartext + DER NÄCHSTE SCHRITT, keine Godot-Texte. ## 5. Deutsch & groß: Schrift ≥ 14 px, klickbare Flächen ≥ 40 px Höhe. @@ -25,6 +39,10 @@ const MoveMath := preload("res://src/editor/move_math.gd") const HelpText := preload("res://src/ui/help_text.gd") # Zentrale UI-Skalierung (Karte t_297221b5): EIN Faktor für Schrift/Panels/Zeilen. const UIScale := preload("res://src/ui/ui_scale.gd") +# U1 (t_eaccbfed): die Registry ist die EINZIGE Layout-Quelle. +const PanelRegistryScript := preload("res://src/ui/panels/registry.gd") +# Editor-Theme (U1): eine Theme-Ressource als einzige Farbquelle. +const THEME_PATH := "res://theme/editor_theme.tres" var manifest: Dictionary = {} var doc: Dictionary = {} # das Map-Dictionary (1:1 svmap-Format) @@ -36,153 +54,97 @@ var history := ActionHistory.new() var interaction # src/editor/interaction.gd (script-erweitert → untypisiert) var gizmo # src/viewport/gizmo.gd (Achsen-Pfeile, Kind von interaction) var gizmo_step_m := 0.5 # Gizmo-Schrittweite in m (Panel-Feld; überlebt UI-Rebuilds) -var gizmo_step_box: SpinBox # SpinBox „Schrittweite“ im rechten Panel var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1) var spawn_tool # src/editor/spawn_tool.gd (Zonen, Karte t_c4162c5d) var build_tool # src/editor/build_tool.gd (Bauen-Modus, Karte t_97ea59c5) var tool_mode := "objects" # "objects" | "terrain" | "spawns" | "bauen" (Modular-Builder) -# Symmetrie-Modus (Karte t_f5935023, M1 Fundament): Modus + Toolbar-Schalter +# Symmetrie-Modus (Karte t_f5935023, M1): Modus + Toolbar-Schalter # + Statuszeilen-Anzeige. Standard = Punktspiegelung (Sly 20.10.). M1 legt nur # Modus + Anzeige fest; das Spiegeln selbst kommt mit M2–M4. -var sym_menu: MenuButton -var sym_label: Label var sym_mode: int = SymmetryCore.Mode.POINT -# UI-Referenzen -var content_tree: Tree -var stats_objects: Label -var stats_terrain: Label +# UI-Referenzen (Koordinator-Sicht auf die Module — Panels bauen selbst) var status_label: Label var file_label: Label var help_label: Label var view_builder: Node3D var camera: Camera3D -var library_box: VBoxContainer -var library_buttons: Dictionary = {} # id → Button -## 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 -const LIBRARY_CFG_SECTION := "library" # Abschnitt in user://scopedvr_editor.cfg +var inspector: Control # Objekt-Inspektor (Karte t_bb0d11c9) — im Inspektor-Tab -# Objekt-Inspektor (Karte t_bb0d11c9) — preload statt class_name, damit die -# Referenz auch ohne frischen Import-Lauf auflöst (Belt-and-braces). -const INSPECTOR_PANEL := preload("res://src/ui/inspector_panel.gd") -const GizmoScript := preload("res://src/viewport/gizmo.gd") +# U1-Module (aus der Registry gebaut; Toolbar/Status/Kern-Widget-Referenzen +# bleiben hier, damit Tests/Tools denselben Vertrag sehen wie vorher) +var toolbar_mod # src/ui/panels/toolbar.gd +var status_mod # src/ui/panels/status_bar.gd +var selection_mod # src/ui/panels/selection_panel.gd +var map_mod # src/ui/panels/map_panel.gd +var terrain_mod # src/ui/panels/terrain_panel.gd +var check_mod # src/ui/panels/check_panel.gd +var tree_mod # src/ui/panels/scene_tree_panel.gd +var assets_mod # src/ui/panels/assets_panel.gd +var content_tree: Tree # weiter in tree_mod (Bequemlichkeits-Referenz) +var stats_objects: Label # weiter in status_mod +var stats_terrain: Label # weiter in status_mod +var tool_label: Label # weiter in status_mod +var sym_label: Label # weiter in status_mod +var undo_btn: Button # weiter in toolbar_mod +var redo_btn: Button # weiter in toolbar_mod +var grid_btn: Button # weiter in toolbar_mod +var contours_btn: Button # weiter in toolbar_mod +var size_menu: MenuButton # weiter in toolbar_mod +var sym_menu: MenuButton # weiter in toolbar_mod +var prop_name: LineEdit # weiter in map_mod +var sel_label: Label # weiter in selection_mod +var gizmo_step_box: SpinBox # weiter in selection_mod +var library_search: LineEdit # weiter in assets_mod +var library_buttons: Dictionary = {} # weiter in assets_mod (Bequemlichkeit) +var _library_group_rows: Array = [] # weiter in assets_mod (Bequemlichkeit) +var spawn_status: Label # weiter in check_mod +var terrain_info: Label # weiter in map_mod +var build_edit_btn: Button # weiter in terrain_mod +var build_panel: VBoxContainer # weiter in terrain_mod +var build_kind_buttons: Dictionary = {} # weiter in terrain_mod +var terrain_mode_lower: Button # weiter in terrain_mod (Test-Vertrag) +var terrain_size_btn_small: Button # weiter in terrain_mod +var terrain_size_btn_medium: Button # weiter in terrain_mod +var terrain_size_btn_large: Button # weiter in terrain_mod +var terrain_palette: VBoxContainer # weiter in terrain_mod +var terrain_radius_slider: HSlider # weiter in terrain_mod +var terrain_radius_label: Label # weiter in terrain_mod +var terrain_strength_slider: HSlider # weiter in terrain_mod +var terrain_mat_buttons: Array = [] # weiter in terrain_mod (Gras/Sand/Fels) +var library_open_all_btn: Button # weiter in assets_mod +var library_close_all_btn: Button # weiter in assets_mod -var undo_btn: Button -var redo_btn: Button -var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014) -var contours_btn: Button # „≈ Höhenlinien“ (Karte t_3ebe4086) +# Toolbar-/Panel-Zustand (gehört dem Koordinator) var _grid_on := false # Nutzer-Wahl für das Maßstabsgitter # Höhenlinien (Karte t_89c0ec95): _contours_on ist die NUTZER-Wahl „fixiert # an“ (L-Taste oder Button). Ohne Fix erscheinen die Linien NUR im # Terrain-Werkzeug (F1) — Sly-Befund 17.10.: dauerhaft sichtbar störte. # Effektiv = _contours_fix AN ODER tool_mode == "terrain". var _contours_on := false - -# Auswahl-Panel -var sel_label: Label -var sel_rotate_btn: Button -var sel_delete_btn: Button -var sel_dup_btn: Button # Objekt-Ops (Karte t_7567a06f) -var sel_mirror_x_btn: Button -var sel_mirror_z_btn: Button -var sel_raise_btn: Button -var sel_lower_btn: Button -var sel_group_btn: Button # „Gruppieren“ (Karte t_f6f29d86) -var sel_ungroup_btn: Button # „Gruppe lösen“ (Karte t_f6f29d86) -var inspector: Control # Objekt-Inspektor (Karte t_bb0d11c9) — oben rechts +var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder # Rahmen-Ausziehen (Rubberband) — Overlay über dem 3D-Viewport var rubber_overlay: Control var rubber_rect := Rect2() # leer = Overlay aus -# Eigenschaften-Panel -var prop_name: LineEdit -var prop_author: LineEdit -var prop_desc: LineEdit -var prop_version: SpinBox -var prop_weather: OptionButton -var terrain_info: Label - -# Terrain-Werkzeug-Panel (Karte t_9aba04f1) -var terrain_edit_btn: Button -var terrain_panel: VBoxContainer -var terrain_mode_raise: Button -var terrain_mode_lower: Button -var terrain_mode_smooth: Button -var terrain_mode_paint: Button -var terrain_palette: VBoxContainer -var terrain_mat_buttons: Array = [] # Gras/Sand/Fels (ButtonGroup) -var terrain_size_btn_small: Button -var terrain_size_btn_medium: Button -var terrain_size_btn_large: Button -var terrain_radius_slider: HSlider -var terrain_radius_label: Label -var terrain_strength_slider: HSlider -var terrain_strength_label: Label - -# Spawn-Zonen-Werkzeug (Karte t_c4162c5d) — großer deutscher Button + Panel -var spawn_edit_btn: Button -var spawn_panel: VBoxContainer -var spawn_team_blue: Button -var spawn_team_red: Button -var spawn_radius_slider: HSlider -var spawn_radius_label: Label -var spawn_status: Label # Live-Validierung (IMMER sichtbar, Loader-Regeln) -var spawn_sel_label: Label # Welche Zone ist an? -var spawn_delete_btn: Button - -# Bauen-Werkzeug (Karte t_97ea59c5) — großer deutscher Button + Bauteil-Panel -var build_edit_btn: Button -var build_panel: VBoxContainer -var build_kind_buttons: Dictionary = {} # kind_id → Button - -# Map-Check (Karte prüfen) -var check_btn: Button -var check_dialog: AcceptDialog -var check_text: Label - -# Karte testen (Karte t_b84af40f) -var test_btn: Button - -# Lauf-Modus „▶ Karte testen" im Builder selbst (Karte t_1cdc1a72): erste -# Person, WASD + Maus — OHNE Client-Ordner, ohne zweiten Prozess, ohne -# VR/OpenXR, ohne Server (Slys verbindliche Vorgabe). Der alte Client-Start -# bleibt als „alt" in src/editor/test_run.gd; der Rúckbau kommt später. +# Playtest (Karte t_1cdc1a72) + Lauf-Modus-Panelverweise var playtest: Node = null # src/editor/playtest/playtest_mode.gd Instanz var _playtest_tool_mode := "" # gemerkter Werkzeug-Modus für die Rückkehr var ui_top_bar: Control # Top-Bar (im Lauf-Modus aus) -var ui_left_panel: Control # linke Bibliothek (im Lauf-Modus aus) -var ui_right_panel: Control # rechte Werkzeug-Panel-Spalte (im Lauf-Modus aus) +var ui_left_panel: Control # linke Spalte (im Lauf-Modus aus) +var ui_right_panel: Control # rechte Tab-Spalte (im Lauf-Modus aus) var ui_status_bar: Control # Statusleiste (im Lauf-Modus aus) +var ui_dock_bottom: Control # Assets-Bereich unten (im Lauf-Modus aus) var world3d: Node3D # 3D-Wurzel im SubViewport (Spieler lebt hier) -# Test-Startpunkt (Karte t_96e3c4ee): „hinklicken, wo man beim Testen spawnt“. -# Editor-seitiger Session-Punkt — Sidecar-Datei (TestStartStore), bewusst -# NICHT im .svmap (Validator V1–V16 + Client/Server unberührt) und KEIN -# Spiel-Spawnpunkt (kein Undo, keine Spawn-Checks/Analysen). null = keiner -# gesetzt → F5 verhält sich exakt wie bisher (erster Team-Spawn, sonst Mitte). -var test_start_point: Variant = null # Vector3 (nur x/z) oder null -var test_start_tool # src/editor/playtest/test_start_tool.gd -var test_start_btn: Button # „🎯 Test-Start“ in der Top-Bar -var test_start_panel: VBoxContainer # rechte Spalte: Info + Löschen -var test_start_info: Label -var test_start_delete_btn: Button - # Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile. var dirty_dialog: ConfirmationDialog # „Ungespeicherte Änderungen verwerfen?“ var _pending_after_dirty := Callable() # was nach „Verwerfen“ ausgeführt wird var help_overlay: PanelContainer # H-Kurzhilfe (nicht-blockierend) var help_overlay_title: Label var help_overlay_text: RichTextLabel -var tool_label: Label # permanenter Werkzeugmodus + Flug-Hint -var size_menu: MenuButton # „UI-Größe“ (Karte t_297221b5): Klein/Mittel/Groß # Editor-Selbstupdate (Karte t_d95f087a): Start-Check + Banner + „Jetzt updaten“ var update_banner: PanelContainer # Leiste „🔄 Update verfügbar …“ (versteckt) @@ -202,8 +164,18 @@ var error_dialog: AcceptDialog var resize_dialog: ConfirmationDialog # „Kartengröße ändern“ (R3, t_c863b93b) var error_text: Label -var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder -var _tree_select_lock := false # verhindert Rückkopplung Auswahl ↔ Baum +# Map-Check (Karte prüfen) +var check_dialog: AcceptDialog +var check_text: Label + +# Karte testen (Karte t_b84af40f) +var test_btn: Button + +# U1-Dock-Zustand: Splitter-Lagen ([fenster]-Abschnitt, gemerkt) +var _dock_offsets: Dictionary = {} + +const GizmoScript := preload("res://src/viewport/gizmo.gd") +const INSPECTOR_PANEL := preload("res://src/ui/inspector_panel.gd") func _ready() -> void: @@ -220,6 +192,7 @@ func _ready() -> void: # Close-Request selbst behandeln, damit die Lage VOR dem Quit gemerkt wird. get_tree().set_auto_accept_quit(false) _apply_window_state() + _load_dock_state() _start_with_new_map() _maybe_start_demo_mode() # Selbst-Update (Karte t_d95f087a): STILLER Start-Check im Hintergrund — @@ -237,234 +210,209 @@ func _load_manifest() -> Dictionary: # ------------------------------------------------------------------ UI-Bau -- +## Layout aus der Registry (U1, Operator-Vorgabe): Top-Bar → Update-Banner → +## Haupt-WSplit (links Tabs | Mitte 3D | rechts Tabs) → unten Assets → +## Statuszeile. Kein Panel wird hier fest verdrahtet — Registry ändert das +## Layout, main.gd bleibt gleich. func _build_ui() -> void: - var theme := Theme.new() - theme.default_font_size = UIScale.font_size() - self.theme = theme + var theme_res: Theme = load(THEME_PATH) + theme_res.default_font_size = UIScale.font_size() + self.theme = theme_res var vbox := VBoxContainer.new() vbox.set_anchors_preset(Control.PRESET_FULL_RECT) vbox.add_theme_constant_override("separation", UIScale.px(4)) add_child(vbox) - # --- Top-Bar: Menü + Rückgängig/Wiederholen --- + # --- Top-Bar (Modul toolbar.gd) --- var top := PanelContainer.new() - var top_h := HBoxContainer.new() - top_h.add_theme_constant_override("separation", UIScale.px(10)) - top.add_child(top_h) + # _make_module hängt das Modul selbst an den Container (doppelt add_child + # würde „already has a parent“ werfen). + var top_h: Control = _make_module(toolbar_script(), top) vbox.add_child(top) ui_top_bar = top # Lauf-Modus (t_1cdc1a72): Top-Bar ausblenden + toolbar_mod = top_h + _pull_toolbar_refs() _update_banner_build(vbox) - var file_menu := MenuButton.new() - file_menu.text = "Datei" - file_menu.flat = false - file_menu.custom_minimum_size = UIScale.v2(90, 40) - var popup := file_menu.get_popup() - popup.add_item("Neue Map…", 0) - popup.add_item("Öffnen…", 1) - popup.add_item("Kartengröße ändern…", 6) - popup.add_separator() - popup.add_item("Speichern (Strg+S)", 2) - popup.add_item("Speichern unter…", 3) - popup.add_separator() - popup.add_item("▶ Karte testen", 5) - popup.add_separator() - popup.add_item("🔄 Updates prüfen", 7) - popup.add_item("Beenden", 4) - popup.id_pressed.connect(_on_menu_action) - top_h.add_child(file_menu) - - undo_btn = Button.new() - undo_btn.text = "↩ Rückgängig" - undo_btn.custom_minimum_size = UIScale.v2(140, 40) - undo_btn.tooltip_text = "Letzte Aktion zurücknehmen (Strg+Z)" - undo_btn.pressed.connect(_do_undo) - top_h.add_child(undo_btn) - - redo_btn = Button.new() - redo_btn.text = "↪ Wiederholen" - redo_btn.custom_minimum_size = UIScale.v2(150, 40) - redo_btn.tooltip_text = "Zurückgenommene Aktion wiederherstellen (Strg+Y)" - redo_btn.pressed.connect(_do_redo) - top_h.add_child(redo_btn) - - # Maßstabsgitter-Toggle (Karte t_7ac70014): über sichtbarem Terrain ist das - # Gitter standardmäßig AUS (das Terrain ist die Bodendarstellung); wer das - # 100-m-Raster als Planungshilfe will, schaltet es hier ohne Rebuild zu. - grid_btn = Button.new() - grid_btn.text = "⊞ Gitter" - grid_btn.toggle_mode = true - grid_btn.button_pressed = false - grid_btn.custom_minimum_size = UIScale.v2(120, 40) - grid_btn.tooltip_text = "Maßstabsgitter (100-m-Raster) im Viewport ein-/ausblenden. Über sichtbarem Terrain ist es standardmäßig aus." - grid_btn.toggled.connect(_on_grid_toggled) - top_h.add_child(grid_btn) - - # Höhenlinien-Button (Karte t_3ebe4086, Kopplung t_89c0ec95): drücken = - # LINIEN FIXIEREN (immer sichtbar, in jedem Werkzeug). Ohne Fix sind die - # Linien NUR im Terrain-Werkzeug (F1) sichtbar; auf flachen Maps (< 2 m - # Gesamtrelief) schaltet das Terrain-Mesh zusätzlich selbst auf aus. - contours_btn = Button.new() - contours_btn.text = "≈ Höhenlinien" - contours_btn.toggle_mode = true - contours_btn.button_pressed = false - contours_btn.custom_minimum_size = UIScale.v2(150, 40) - contours_btn.tooltip_text = "Höhenlinien (1 m, alle 5 m kräftiger) FIXIERT an/aus. Ohne Fixierung erscheinen sie nur im Terrain-Werkzeug (F1). Taste L schaltet ebenfalls. Bei flachem Terrain (< 2 m) sind sie automatisch aus." - contours_btn.toggled.connect(_on_contours_toggled) - top_h.add_child(contours_btn) - - # Symmetrie-Schalter (Karte t_f5935023, M1): Aus / Punkt / Achse X / Achse Z. - # Standard = Punktspiegelung (Sly 20.10.). M1 setzt nur Modus + Anzeige; - # das gespiegelte Bauen selbst kommt mit M2–M4. - sym_menu = MenuButton.new() - sym_menu.text = "Symmetrie: Punkt" - sym_menu.flat = false - sym_menu.custom_minimum_size = UIScale.v2(160, 40) - sym_menu.tooltip_text = "Gespiegeltes Bauen: Änderungen werden automatisch auf die andere Kartenseite übertragen. Aus / Punkt (180° um die Kartenmitte) / Achse X / Achse Z. Standard: Punkt." - var sym_popup := sym_menu.get_popup() - for i in SymmetryCore.MODE_LABELS.size(): - sym_popup.add_check_item(SymmetryCore.MODE_LABELS[i], i) - sym_popup.index_pressed.connect(_on_sym_mode_selected) - top_h.add_child(sym_menu) - - # Map-Fertig-Check (Karte t_c4162c5d): Klartext-Liste VOR dem Export. - check_btn = Button.new() - check_btn.text = "✅ Karte prüfen" - check_btn.custom_minimum_size = UIScale.v2(170, 40) - check_btn.tooltip_text = "Prüft, ob die Map bereit für den Export ist — sagen wir VORHER, was fehlt" - check_btn.pressed.connect(_on_check_map) - top_h.add_child(check_btn) - - # Karte testen (Karte t_b84af40f): Map SOFORT als Spiel zum Durchlaufen. - test_btn = Button.new() - test_btn.text = "▶ Karte testen" - test_btn.custom_minimum_size = UIScale.v2(190, 40) - test_btn.tooltip_text = "Startet die Map SOFORT als Spiel zum Durchlaufen — eigenes Fenster, der Editor bleibt offen. ESC im Spiel = zurück zum Editor." - test_btn.pressed.connect(_on_test_run) - top_h.add_child(test_btn) - - # Test-Startpunkt (Karte t_96e3c4ee): Taste T oder dieser Knopf aktiviert - # das Werkzeug — ein Klick in die Karte setzt den Punkt, an dem F5 den - # Testlauf startet (KEIN Spiel-Spawnpunkt, kein .svmap-Eintrag). - test_start_btn = Button.new() - test_start_btn.text = "🎯 Test-Start" - test_start_btn.toggle_mode = true - test_start_btn.custom_minimum_size = UIScale.v2(160, 40) - test_start_btn.tooltip_text = "Test-Startpunkt setzen (Taste T): Klicke in die Karte, wo der Testlauf (F5) starten soll. Kein Spiel-Spawnpunkt — gilt nur fürs Testen im Editor." - test_start_btn.pressed.connect(_on_test_start_btn_pressed) - top_h.add_child(test_start_btn) - - # UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um, - # es wirkt SOFORT (kompletter UI-Neuaufbau mit dem neuen Faktor) und bleibt - # in user://scopedvr_editor.cfg gespeichert. - size_menu = MenuButton.new() - size_menu.text = "UI-Größe" - size_menu.flat = false - size_menu.custom_minimum_size = UIScale.v2(120, 40) - size_menu.tooltip_text = "Größe der Oberfläche: Klein (wie bisher), Mittel (1,25-fach), Groß (1,5-fach). Wirkt sofort und bleibt gespeichert." - var size_popup := size_menu.get_popup() - for i in UIScale.step_count(): - size_popup.add_check_item(UIScale.step_label(i), i) - _sync_size_menu() - size_popup.index_pressed.connect(_on_ui_size_selected) - top_h.add_child(size_menu) - - var title_label := Label.new() - title_label.text = " %s" % APP_TITLE - title_label.add_theme_font_size_override("font_size", UIScale.f(20)) - # Karte t_297221b5: bei großer Stufe darf der Titel EGALISIEREN statt die - # Top-Bar aus dem Fenster zu drücken (Mindestbreite der Schrift skaliert mit). - # Mindestbreite, damit er nicht KOMPLETT verschwindet (Ellipsis macht "Scop…"). - # Expand: bei Platz bekommt er seine VOLLSTÄNDIGE natürliche Breite (wie - # Vorher bei „Klein“), erst bei knappem Fenster egalisiert er mit "…". - title_label.custom_minimum_size = UIScale.v2(180, 0) - title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - title_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - title_label.clip_text = true - top_h.add_child(title_label) - - var spacer := Control.new() - spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL - top_h.add_child(spacer) - - file_label = Label.new() - file_label.text = "" - file_label.modulate = Color(1, 1, 1, 0.6) - # Karte t_297221b5: Map-Pfad darf bei großer Stufe egalisiert werden - # (Voller Name im Tooltip, sonst drückt die Zeile die Top-Bar aus dem Fenster). - # Expand: bekommt zusammen mit dem Spacer seinen Anteil, egalisiert mit "…". - file_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - file_label.clip_text = true - file_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - top_h.add_child(file_label) - - # --- Hauptbereich --- + # --- Hauptbereich: links Tabs | Mitte 3D | rechts Tabs --- var split := HSplitContainer.new() split.size_flags_vertical = Control.SIZE_EXPAND_FILL split.size_flags_horizontal = Control.SIZE_EXPAND_FILL split.split_offset = UIScale.px(-320) + split.name = "MainSplit" vbox.add_child(split) # Lauf-Modus (t_1cdc1a72): linke/rechte Spalte als Member — werden im # Lauf-Modus ausgeblendet, die 3D-Ansicht (Mitte) bleibt stehen. - var left := _build_left_panel() - ui_left_panel = left - split.add_child(left) + var left_tabs := _build_group_tabs("links") + left_tabs.name = "LeftTabs" + ui_left_panel = left_tabs + split.add_child(left_tabs) split.add_child(_build_center_viewport()) - var right := _build_right_panel() - ui_right_panel = right - split.add_child(right) + var right_tabs := _build_group_tabs("rechts") + right_tabs.name = "RightTabs" + ui_right_panel = right_tabs + split.add_child(right_tabs) - # --- Statusleiste: Hilfs-Meldungen + Werkzeug-Info --- + # --- Assets unten (Registry-Gruppe "unten", U1 Auftragspunkt 4) --- + # Die Objekt-Bibliothek wohnt jetzt UNTER dem Hauptbereich (Unity- + # Projekt-Fenster-Platz). Höhe größenverstellbar über den VSplit und + # gemerkt in [fenster] (dock_unten). + var bottom_wrap := VSplitContainer.new() + bottom_wrap.size_flags_vertical = Control.SIZE_EXPAND_FILL + bottom_wrap.size_flags_horizontal = Control.SIZE_EXPAND_FILL + bottom_wrap.name = "BottomSplit" + # WICHTIG: den Haupt-Split ERST aus der VBox trennen, dann in den VSplit + # hängen — add_child auf ein Kind mit Parent ist ein stiller Baum-Fehler + # (Node hängt dann im Nichts: Slider/Skript-Signale laufen nicht). + vbox.remove_child(split) + # VSplit-Kind 1: der Haupt-Split (links|3D|rechts) — er wächst. + split.size_flags_vertical = Control.SIZE_EXPAND_FILL + bottom_wrap.add_child(split) + # VSplit-Kind 2: die Assets-Tabs. + var bottom_tabs := _build_group_tabs("unten") + bottom_tabs.name = "BottomTabs" + bottom_tabs.custom_minimum_size = UIScale.v2(0, 300) + var bottom_panel := PanelContainer.new() + bottom_panel.add_child(bottom_tabs) + bottom_wrap.add_child(bottom_panel) + # Der VSplit ersetzt den Platz des bisherigen Haupt-Splits in der VBox. + vbox.add_child(bottom_wrap) + ui_dock_bottom = bottom_wrap + + # --- Statusleiste (Modul status_bar.gd) --- var status_panel := PanelContainer.new() - var status_h := HBoxContainer.new() - status_h.add_theme_constant_override("separation", UIScale.px(16)) - status_panel.add_child(status_h) + var status_h: Control = _make_module(status_bar_script(), status_panel) vbox.add_child(status_panel) ui_status_bar = status_panel # Lauf-Modus (t_1cdc1a72): Statusleiste aus + status_mod = status_h + _pull_status_refs() + status_label = status_mod.status_label + file_label = toolbar_mod.file_label - status_label = Label.new() - status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - status_label.clip_text = true # langer Text darf das Layout NICHT sprengen - status_h.add_child(status_label) + _build_dialogs_and_overlays() - # Karte 3/3 (t_11b5c25b): PERMANENTE Werkzeug-Anzeige — Sly sieht immer, - # in welchem Werkzeug er ist, plus Hint für den Flugmodus (Umschalt+F). - tool_label = Label.new() - tool_label.modulate = Color(0.55, 0.8, 1.0) - # Karte t_297221b5: Statuszeilen-Labels egalisiert (min-size-Druck bei 1.5×) - tool_label.clip_text = true - tool_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_h.add_child(tool_label) - # Symmetrie-Anzeige (Karte t_f5935023, M1): Sly sieht in der Statuszeile - # immer, ob/wie gespiegelt wird (Standard: Punkt). - sym_label = Label.new() - sym_label.modulate = Color(0.55, 0.8, 1.0) - sym_label.clip_text = true - sym_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_h.add_child(sym_label) +func toolbar_script() -> GDScript: + return load("res://src/ui/panels/toolbar.gd") - stats_objects = Label.new() - stats_objects.modulate = Color(1, 1, 1, 0.75) - stats_objects.clip_text = true - stats_objects.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_h.add_child(stats_objects) - # Terrain-Anteil + Editor-FPS (Perf-Zähler, Karte t_9aba04f1) - stats_terrain = Label.new() - stats_terrain.modulate = Color(1, 1, 1, 0.75) - stats_terrain.clip_text = true - stats_terrain.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_h.add_child(stats_terrain) - var fps_timer := Timer.new() - fps_timer.wait_time = 0.5 - fps_timer.autostart = true - fps_timer.timeout.connect(_refresh_stats) - add_child(fps_timer) +func status_bar_script() -> GDScript: + return load("res://src/ui/panels/status_bar.gd") + +## Ein Modul (EditorPanel-Ableger) instanziieren und bauen; Rückgabe = das +## Panel selbst (Container). Fehler im Modul-Skript = fette Meldung statt +## stiller null-Referenzen. +func _make_module(script: GDScript, parent: Control) -> Control: + var mod: Control = script.new() + parent.add_child(mod) + if mod.has_method("setup"): + mod.setup(self) + if mod.has_method("build"): + mod.call("build") + return mod + + +func _pull_toolbar_refs() -> void: + undo_btn = toolbar_mod.undo_btn + redo_btn = toolbar_mod.redo_btn + grid_btn = toolbar_mod.grid_btn + contours_btn = toolbar_mod.contours_btn + size_menu = toolbar_mod.size_menu + sym_menu = toolbar_mod.sym_menu + # Häkchen im UI-Größe-Menü auf die aktuelle Stufe (vor U1 rief die + # Top-Bar _sync_size_menu während des Baus — jetzt nach dem Referenz-Zug). + _sync_size_menu() + + +func _pull_status_refs() -> void: + stats_objects = status_mod.stats_objects + stats_terrain = status_mod.stats_terrain + tool_label = status_mod.tool_label + sym_label = status_mod.sym_label + status_label = status_mod.status_label + + +## Eine Tab-Gruppe aus der Registry bauen (links/rechts/unten). Jeder Bereich +## erscheint GENAU EINMAL (test_ui_layout prüft das). Die Panels werden per +## preload aus dem Registry-Eintrag erzeugt. +func _build_group_tabs(gruppe: String) -> TabContainer: + var tabs := TabContainer.new() + tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL + tabs.size_flags_vertical = Control.SIZE_EXPAND_FILL + tabs.add_theme_constant_override("separation", UIScale.px(8)) + var entries: Array = PanelRegistryScript.entries(gruppe) + for entry in entries: + var script: GDScript = load(str(entry["skript"])) + var panel: Control = script.new() + panel.name = str(entry["id"]) + panel.setup(self) + # Der Alt-Inspektor (t_bb0d11c9) baut in setup() — build() ist dort + # nicht vorhanden; neue Module bauen in build(). Beides zulassen. + if panel.has_method("build"): + panel.call("build") + tabs.add_child(panel) + tabs.set_tab_title(panel.get_index(), str(entry["titel"])) + _panel_group_built(gruppe, tabs) + return tabs + + +## Gruppen-Nacharbeit: Referenzen ziehen, sobald ihre Tabs stehen. +func _panel_group_built(gruppe: String, tabs: TabContainer) -> void: + match gruppe: + "rechts": + for child in tabs.get_children(): + match child.name: + "inspektor": + inspector = child + "auswahl": + selection_mod = child + "map": + map_mod = child + "terrain": + terrain_mod = child + "check": + check_mod = child + sel_label = selection_mod.sel_label + gizmo_step_box = selection_mod.gizmo_step_box + prop_name = map_mod.prop_name + spawn_status = check_mod.spawn_status + terrain_info = map_mod.terrain_info + build_edit_btn = terrain_mod.build_edit_btn + build_panel = terrain_mod.build_panel + build_kind_buttons = terrain_mod.build_kind_buttons + # Test-/Werkzeug-Vertrag (wie vor der U1-Aufteilung): die Widgets + # wohnen in den Modulen, der Koordinator hält die alten Namen + # als Bequemlichkeits-Referenzen — nichts wird doppelt gebaut. + terrain_mode_lower = terrain_mod.terrain_mode_lower + terrain_size_btn_small = terrain_mod.terrain_size_btn_small + terrain_size_btn_medium = terrain_mod.terrain_size_btn_medium + terrain_size_btn_large = terrain_mod.terrain_size_btn_large + terrain_palette = terrain_mod.terrain_palette + terrain_radius_slider = terrain_mod.terrain_radius_slider + terrain_radius_label = terrain_mod.terrain_radius_label + terrain_strength_slider = terrain_mod.terrain_strength_slider + terrain_mat_buttons = terrain_mod.terrain_mat_buttons + "links": + for child in tabs.get_children(): + if child.name == "szene": + tree_mod = child + content_tree = tree_mod.content_tree + "unten": + for child in tabs.get_children(): + if child.name == "assets": + assets_mod = child + library_search = assets_mod.library_search + library_buttons = assets_mod.library_buttons + _library_group_rows = assets_mod._library_group_rows + library_open_all_btn = assets_mod.library_open_all_btn + library_close_all_btn = assets_mod.library_close_all_btn + + +func _build_dialogs_and_overlays() -> void: # --- Dialoge --- new_dialog = preload("res://src/ui/new_map_dialog.gd").new() new_dialog.confirmed.connect(_on_new_map_confirmed) @@ -560,279 +508,6 @@ func _build_ui() -> void: add_child(help_overlay) -func _build_left_panel() -> PanelContainer: - var panel := PanelContainer.new() - panel.custom_minimum_size = UIScale.v2(280, 0) - # Karte t_297221b5: das GESAMTE linke Panel scrollt, wenn es bei großer - # UI-Stufe höher als das Fenster wird (wie die rechte Spalte es schon tut). - # Bei „Klein“ (alles passt) bleibt das Verhalten exakt wie vorher: der - # innere Inhalt Expand-FILL't, die Bibliothek nutzt den ganzen Platz. - var panel_scroll := ScrollContainer.new() - panel_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - panel.add_child(panel_scroll) - var vbox := VBoxContainer.new() - vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL - vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL - vbox.add_theme_constant_override("separation", UIScale.px(6)) - panel_scroll.add_child(vbox) - - vbox.add_child(_header_label("Objekt-Bibliothek")) - vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis ESC (Rechte Maustaste = Flug).")) - - # Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit - # Treffern klappen auf, leere Gruppen verschwinden. - 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()) - vbox.add_child(library_search) - - # „Alle auf" / „Alle zu" (t_59fc0f21): Klapp-Zustand aller Gruppen auf einmal. - var group_btns := HBoxContainer.new() - group_btns.add_theme_constant_override("separation", UIScale.px(6)) - 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) - vbox.add_child(group_btns) - - 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 - vbox.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) - _fill_library() - - vbox.add_child(_separator()) - vbox.add_child(_header_label("Objekt-Werkzeuge")) - - var snap_btn := Button.new() - snap_btn.text = "⚡ Alles auf den Boden setzen" - snap_btn.custom_minimum_size = UIScale.v2(0, 44) - snap_btn.tooltip_text = "Setzt ALLE Objekte auf die Terrain-Höhe — z. B. nach dem Terrain-Formen, wenn etwas in der Luft hängt" - snap_btn.pressed.connect(_on_snap_to_ground) - vbox.add_child(snap_btn) - - var grid_check := CheckButton.new() - grid_check.text = "Raster-Snap (1 m)" - grid_check.custom_minimum_size = UIScale.v2(0, 40) - grid_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein" - grid_check.toggled.connect(_on_grid_snap_changed) - vbox.add_child(grid_check) - vbox.add_child(_muted_label("Raster-Snap rastet Platzieren und Ziehen auf ganze Meter.")) - - vbox.add_child(_separator()) - vbox.add_child(_header_label("Map-Inhalt")) - - content_tree = Tree.new() - content_tree.size_flags_vertical = Control.SIZE_EXPAND_FILL - content_tree.custom_minimum_size = UIScale.v2(0, 160) - content_tree.allow_reselect = true - content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2) - content_tree.hide_root = true - content_tree.item_selected.connect(_on_tree_selected) - vbox.add_child(content_tree) - return panel - - -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). - var first_open: String = "" - for group in LibraryBrowser.groups_in_order(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.f(18)) - 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(6)) - grid.add_theme_constant_override("v_separation", UIScale.px(6)) - 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()}) - _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: - (row["header"] as Button).button_pressed = true - - -func _on_library_collapse_all() -> void: - for row in _library_group_rows: - (row["header"] as Button).button_pressed = false - - -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): - out[str(key)] = bool(cfg.get_value(LIBRARY_CFG_SECTION, 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") - - -## Live-Suche (t_59fc0f21): leer = alles normal sichtbar (Klapp-Zustand wie -## gemerkt); Text = nur Treffer (Name de/en, ID — case-insensitive), Gruppen -## mit Treffern bleiben offen, leere verschwinden. -func _apply_library_filter(query: String) -> void: - var q := query.strip_edges().to_lower() - var searching := not q.is_empty() - for row in _library_group_rows: - var ids: Array = row["ids"] - var any := false - for oid in ids: - var btn := library_buttons.get(str(oid)) as Button - if btn == null: - continue - var vis := true - if searching: - vis = _library_oid_matches(str(oid), q) - btn.visible = vis - any = any or vis - var header := row["header"] as Button - var box := row["box"] as VBoxContainer - header.visible = any - if searching: - # Treffer-Gruppen bleiben auf — auch wenn sie zugeklappt waren - # (Pfeil zeigt dann offen, kosmetisch konsistent). - box.visible = any - if any: - header.text = _library_header_text(true, str(row["label"]), int(row["count"])) - else: - var is_open := bool(_library_group_open.get(str(row["key"]), false)) - box.visible = is_open - header.text = _library_header_text(is_open, str(row["label"]), int(row["count"])) - - -func _library_oid_matches(oid: String, q: String) -> bool: - if oid.to_lower().contains(q): - return true - var entry: Dictionary = (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 - - -func _add_library_button(oid: String, parent: Container) -> void: - var lib: Dictionary = 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 btn := Button.new() - btn.toggle_mode = true - btn.button_pressed = false - btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL - btn.custom_minimum_size = UIScale.v2(0, 126) - btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid] - btn.pressed.connect(_on_library_pressed.bind(oid)) - 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): 96-px-Vorschaubild oben, Name klein darunter. - # Kinder ignorieren die Maus, damit die Klicks am Button ankommen. - var tile := VBoxContainer.new() - tile.mouse_filter = Control.MOUSE_FILTER_IGNORE - tile.set_anchors_preset(Control.PRESET_FULL_RECT) - tile.add_theme_constant_override("separation", UIScale.px(2)) - btn.add_child(tile) - 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 lbl := Label.new() - lbl.text = name_txt - lbl.add_theme_font_size_override("font_size", UIScale.f(11)) - 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) - parent.add_child(btn) - library_buttons[oid] = btn - - func _build_center_viewport() -> SubViewportContainer: var container := SubViewportContainer.new() container.stretch = true @@ -892,13 +567,6 @@ func _build_center_viewport() -> SubViewportContainer: world.add_child(build_tool) build_tool.setup(self, camera, view_builder, interaction) - # Test-Startpunkt-Werkzeug (Karte t_96e3c4ee) — ditto eigener Eingabe-Knoten - # (Taste T / Toolbar): Klick setzt den Test-Start, Marker bleibt sichtbar. - test_start_tool = Node3D.new() - test_start_tool.set_script(load("res://src/editor/playtest/test_start_tool.gd")) - world.add_child(test_start_tool) - test_start_tool.setup(self, camera, view_builder, interaction) - # Bedien-Hinweis im Viewport (Overlay) help_label = Label.new() help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück" @@ -949,431 +617,6 @@ func _draw_rubberband() -> void: return rubber_overlay.draw_rect(rubber_rect, Color(0.45, 0.8, 1.0, 0.16), true) rubber_overlay.draw_rect(rubber_rect, Color(0.45, 0.8, 1.0, 0.9), false, 2.0) - - -func _build_right_panel() -> PanelContainer: - var panel := PanelContainer.new() - panel.custom_minimum_size = UIScale.v2(320, 0) - var scroll := ScrollContainer.new() - scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - panel.add_child(scroll) - var vbox := VBoxContainer.new() - vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL - vbox.add_theme_constant_override("separation", UIScale.px(8)) - scroll.add_child(vbox) - - # --- Objekt-Inspektor (Karte t_bb0d11c9): ERSTER Abschnitt, so wie der - # Inspector in Godot/Unity — erscheint beim Anklicken und zeigt - # Position/Drehung/Größe tippbar UND ziehbar (Widget-Logik in - # inspector_panel.gd). Verdrahtung über _selection_status + - # _sync_selection_panel (alle Auswahl-Wege laufen dort zusammen). - inspector = INSPECTOR_PANEL.new() - inspector.setup(self) - vbox.add_child(inspector) - vbox.add_child(_separator()) - - # --- Auswahl (Anfassen/Greifen) --- - vbox.add_child(_header_label("Auswahl")) - sel_label = Label.new() - sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an." - sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - vbox.add_child(sel_label) - var sel_btns := HBoxContainer.new() - sel_btns.add_theme_constant_override("separation", UIScale.px(8)) - vbox.add_child(sel_btns) - sel_rotate_btn = Button.new() - sel_rotate_btn.text = "Drehen (R)" - sel_rotate_btn.custom_minimum_size = UIScale.v2(140, 40) - sel_rotate_btn.tooltip_text = "Ausgewähltes Objekt 15° drehen (auch: Mausrad)" - sel_rotate_btn.pressed.connect(func() -> void: interaction.rotate_selected(15.0)) - sel_btns.add_child(sel_rotate_btn) - sel_delete_btn = Button.new() - sel_delete_btn.text = "Löschen (Entf)" - sel_delete_btn.custom_minimum_size = UIScale.v2(140, 40) - sel_delete_btn.tooltip_text = "Ausgewähltes Objekt löschen (Strg+Z holt es zurück)" - sel_delete_btn.pressed.connect(interaction.delete_selected) - sel_btns.add_child(sel_delete_btn) - - # Objekt-Ops (Karte t_7567a06f): Duplizieren · Spiegeln · Anheben/Absenken. - var dup_row := HBoxContainer.new() - dup_row.add_theme_constant_override("separation", UIScale.px(8)) - vbox.add_child(dup_row) - sel_dup_btn = _sel_op_button("⧉ Duplizieren (Strg+D)", - "Legt eine Kopie daneben — leicht versetzt, direkt angewählt. Strg+D geht auch.", - func() -> void: interaction.duplicate_selected()) - dup_row.add_child(sel_dup_btn) - - var mirror_row := HBoxContainer.new() - mirror_row.add_theme_constant_override("separation", UIScale.px(8)) - vbox.add_child(mirror_row) - sel_mirror_x_btn = _sel_op_button("⇄ Links↔Rechts", - "Spiegelt das Objekt an seiner Stelle (links↔rechts, X-Achse) — die Blickrichtung wird mitgespiegelt.", - func() -> void: interaction.mirror_selected("x")) - mirror_row.add_child(sel_mirror_x_btn) - sel_mirror_z_btn = _sel_op_button("⇅ Vor↔Zurück", - "Spiegelt das Objekt an seiner Stelle (vor↔zurück, Z-Achse) — die Blickrichtung wird mitgespiegelt.", - func() -> void: interaction.mirror_selected("z")) - mirror_row.add_child(sel_mirror_z_btn) - - var lift_row := HBoxContainer.new() - lift_row.add_theme_constant_override("separation", UIScale.px(8)) - vbox.add_child(lift_row) - sel_raise_btn = _sel_op_button("▲ Anheben (Bild↑)", - "Objekt 0,25 m anheben — es schwebt dann, auch wenn der Boden darunter anders ist. Bild↑ geht auch.", - func() -> void: interaction.lift_selected(ObjectOps.LIFT_STEP_M)) - lift_row.add_child(sel_raise_btn) - sel_lower_btn = _sel_op_button("▼ Absenken (Bild↓)", - "Objekt 0,25 m absenken — stoppt am Boden (nichts wird in Hügel begraben). Bild↓ geht auch.", - func() -> void: interaction.lift_selected(-ObjectOps.LIFT_STEP_M)) - lift_row.add_child(sel_lower_btn) - - # Gruppen-Buttons (Karte t_f6f29d86): Mehrfachauswahl → Gruppe - var group_btns := HBoxContainer.new() - group_btns.add_theme_constant_override("separation", UIScale.px(8)) - vbox.add_child(group_btns) - sel_group_btn = Button.new() - sel_group_btn.text = "🔗 Gruppieren" - sel_group_btn.custom_minimum_size = UIScale.v2(150, 40) - sel_group_btn.tooltip_text = "Ausgewählte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam" - sel_group_btn.pressed.connect(func() -> void: interaction.group_selection()) - group_btns.add_child(sel_group_btn) - sel_ungroup_btn = Button.new() - sel_ungroup_btn.text = "⛓ Gruppe lösen" - sel_ungroup_btn.custom_minimum_size = UIScale.v2(150, 40) - sel_ungroup_btn.tooltip_text = "Die Gruppe der Auswahl wieder auflösen (Strg+Umschalt+G) — Objekte bleiben, nur die Bindung löst sich" - sel_ungroup_btn.pressed.connect(func() -> void: interaction.ungroup_selection()) - group_btns.add_child(sel_ungroup_btn) - vbox.add_child(_muted_label("Mehrfachauswahl: Umschalt+Klick oder Rahmen aufziehen. Eine Gruppe wandert und dreht gemeinsam — auch nach Speichern/Laden.")) - - # --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen- - # Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die - # Voreinstellungen (fein/normal/grob); dieses Feld setzt sie exakt und - # überlebt UI-Rebuilds, weil der Wert in gizmo_step_m liegen bleibt. - vbox.add_child(_header_label("Gizmo-Einstellungen")) - var gizmo_row := HBoxContainer.new() - gizmo_row.add_theme_constant_override("separation", UIScale.px(8)) - vbox.add_child(gizmo_row) - gizmo_row.add_child(_field_label("Schrittweite")) - gizmo_step_box = SpinBox.new() - gizmo_step_box.min_value = 0.01 - gizmo_step_box.max_value = 10.0 - gizmo_step_box.step = 0.01 - gizmo_step_box.value = gizmo_step_m - gizmo_step_box.custom_minimum_size = UIScale.v2(110, 30) - gizmo_step_box.tooltip_text = "Wie weit bewegt sich das Objekt am Achsen-Gizmo pro Schritt (in Metern). Mausrad WÄHREND des Ziehens wechselt zwischen fein (0.1) / normal (0.5) / grob (2.0)." - gizmo_row.add_child(gizmo_step_box) - gizmo_step_box.value_changed.connect(func(v: float) -> void: - gizmo_step_m = v - if gizmo != null and is_instance_valid(gizmo): - gizmo.set_step_m(v) - ) - - vbox.add_child(_separator()) - - # --- Map-Eigenschaften --- - vbox.add_child(_header_label("Map-Eigenschaften")) - - vbox.add_child(_field_label("Name")) - prop_name = LineEdit.new() - prop_name.max_length = 64 - vbox.add_child(prop_name) - - vbox.add_child(_field_label("Autor")) - prop_author = LineEdit.new() - vbox.add_child(prop_author) - - vbox.add_child(_field_label("Beschreibung")) - prop_desc = LineEdit.new() - vbox.add_child(prop_desc) - - vbox.add_child(_field_label("Version")) - prop_version = SpinBox.new() - prop_version.min_value = 1 - prop_version.max_value = 99999 - prop_version.step = 1 - vbox.add_child(prop_version) - - vbox.add_child(_field_label("Wetter")) - prop_weather = OptionButton.new() - prop_weather.add_item("Mittag (sonnig, klar)") - prop_weather.add_item("Abend (warmes Licht)") - prop_weather.add_item("Regen (grau, kühl)") - prop_weather.add_item("Nacht (dunkel)") - vbox.add_child(prop_weather) - - vbox.add_child(_separator()) - vbox.add_child(_header_label("Terrain")) - terrain_info = Label.new() - terrain_info.text = "" - terrain_info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - vbox.add_child(terrain_info) - - # --- Terrain-Werkzeug (Karte t_9aba04f1): großer deutscher Button --- - terrain_edit_btn = Button.new() - terrain_edit_btn.text = "⛰ Terrain formen" - terrain_edit_btn.custom_minimum_size = UIScale.v2(0, 48) - terrain_edit_btn.toggle_mode = true - terrain_edit_btn.tooltip_text = "Boden heben, senken und glätten — direkt in der Karte malen" - terrain_edit_btn.pressed.connect(_on_terrain_btn_pressed) - vbox.add_child(terrain_edit_btn) - - terrain_panel = VBoxContainer.new() - terrain_panel.add_theme_constant_override("separation", UIScale.px(6)) - terrain_panel.visible = false - vbox.add_child(terrain_panel) - - terrain_panel.add_child(_field_label("Pinsel-Werkzeug:")) - var mode_row := HBoxContainer.new() - mode_row.add_theme_constant_override("separation", UIScale.px(6)) - terrain_panel.add_child(mode_row) - var mode_group := ButtonGroup.new() - terrain_mode_raise = _terrain_mode_button("Heben ⬆", mode_group) - terrain_mode_lower = _terrain_mode_button("Senken ⬇", mode_group) - terrain_mode_smooth = _terrain_mode_button("Glätten ≈", mode_group) - terrain_mode_paint = _terrain_mode_button("Malen 🖌", mode_group) - terrain_mode_raise.button_pressed = true - terrain_mode_raise.toggled.connect(_on_terrain_mode_changed) - terrain_mode_lower.toggled.connect(_on_terrain_mode_changed) - terrain_mode_smooth.toggled.connect(_on_terrain_mode_changed) - terrain_mode_paint.toggled.connect(_on_terrain_mode_changed) - mode_row.add_child(terrain_mode_raise) - mode_row.add_child(terrain_mode_lower) - mode_row.add_child(terrain_mode_smooth) - mode_row.add_child(terrain_mode_paint) - - # Material-Palette (Karte t_603ec1a8): nur sichtbar im Malen-Modus. - terrain_palette = VBoxContainer.new() - terrain_palette.add_theme_constant_override("separation", UIScale.px(4)) - terrain_palette.visible = false - terrain_panel.add_child(terrain_palette) - terrain_palette.add_child(_field_label("Material:")) - var mat_row := HBoxContainer.new() - mat_row.add_theme_constant_override("separation", UIScale.px(6)) - terrain_palette.add_child(mat_row) - var mat_group := ButtonGroup.new() - var mat_labels := ["🌿 Gras", "🏖 Sand", "🪨 Fels"] - for mat_id in SvmapIO.MATERIAL_IDS.size(): - var mat_btn := Button.new() - mat_btn.text = str(mat_labels[mat_id]) - mat_btn.custom_minimum_size = UIScale.v2(0, 40) - mat_btn.toggle_mode = true - mat_btn.button_group = mat_group - mat_btn.tooltip_text = "Malen: %s" % str(SvmapIO.MATERIAL_IDS[mat_id]) - if mat_id == 0: - mat_btn.button_pressed = true - mat_btn.toggled.connect(_on_terrain_material_changed.bind(mat_id)) - terrain_mat_buttons.append(mat_btn) - mat_row.add_child(mat_btn) - - # Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider. - terrain_panel.add_child(_field_label("Pinselgröße:")) - var size_row := HBoxContainer.new() - size_row.add_theme_constant_override("separation", UIScale.px(6)) - terrain_panel.add_child(size_row) - var size_group := ButtonGroup.new() - terrain_size_btn_small = _terrain_size_button("Klein\n(4 m)", size_group, 4.0) - terrain_size_btn_medium = _terrain_size_button("Mittel\n(12 m)", size_group, 12.0) - terrain_size_btn_large = _terrain_size_button("Groß\n(32 m)", size_group, 32.0) - # Kein Signal: Der Handler greift auf terrain_radius_slider zu, der erst - # unten erzeugt wird — beim Panel-Bau wäre das ein Crash (GUI-Start-Bug - # 15.10.). Der Slider startet unten passend mit 12 m (= Mittel). - terrain_size_btn_medium.set_pressed_no_signal(true) - size_row.add_child(terrain_size_btn_small) - size_row.add_child(terrain_size_btn_medium) - size_row.add_child(terrain_size_btn_large) - - terrain_radius_label = _field_label("Pinsel-Größe: 12 m") - terrain_panel.add_child(terrain_radius_label) - terrain_radius_slider = HSlider.new() - terrain_radius_slider.min_value = 2.0 - terrain_radius_slider.max_value = 64.0 - terrain_radius_slider.step = 1.0 - terrain_radius_slider.value = 12.0 - terrain_radius_slider.custom_minimum_size = UIScale.v2(0, 32) - terrain_radius_slider.value_changed.connect(_on_terrain_radius_changed) - terrain_panel.add_child(terrain_radius_slider) - - terrain_strength_label = _field_label("Stärke: 8 m pro Sekunde") - terrain_panel.add_child(terrain_strength_label) - terrain_strength_slider = HSlider.new() - terrain_strength_slider.min_value = 1.0 - terrain_strength_slider.max_value = 20.0 - terrain_strength_slider.step = 0.5 - terrain_strength_slider.value = 8.0 - terrain_strength_slider.custom_minimum_size = UIScale.v2(0, 32) - terrain_strength_slider.value_changed.connect(_on_terrain_strength_changed) - terrain_panel.add_child(terrain_strength_slider) - - terrain_panel.add_child(_muted_label( - "Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Strg+Linksklick macht das Gegenteil — im Malen-Modus malt Strg+Klick Gras zurück. " - + "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). Rechte Maustaste = Flug, ESC beendet den Terrain-Modus.")) - - # --- Spawn-Zonen (Karte t_c4162c5d): Live-Status IMMER sichtbar --- - vbox.add_child(_separator()) - vbox.add_child(_header_label("Spawn-Check")) - spawn_status = Label.new() - spawn_status.text = "" - spawn_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - spawn_status.custom_minimum_size = UIScale.v2(0, 44) - vbox.add_child(spawn_status) - vbox.add_child(_muted_label("Der Check sagt VOR dem Export, was der Server-Loader verlangen wird (mindestens 4 Zonen pro Team, bekannte Objekt-IDs).")) - - # --- Spawn-Zonen-Werkzeug: großer Button + Bedien-Panel --- - spawn_edit_btn = Button.new() - spawn_edit_btn.text = "🚩 Spawn-Zonen malen" - spawn_edit_btn.custom_minimum_size = UIScale.v2(0, 48) - spawn_edit_btn.toggle_mode = true - spawn_edit_btn.tooltip_text = "Startzonen für Team Blau und Rot auf den Boden malen (TDM)" - spawn_edit_btn.pressed.connect(_on_spawn_btn_pressed) - vbox.add_child(spawn_edit_btn) - - spawn_panel = VBoxContainer.new() - spawn_panel.add_theme_constant_override("separation", UIScale.px(6)) - spawn_panel.visible = false - vbox.add_child(spawn_panel) - - spawn_panel.add_child(_field_label("Team:")) - var team_row := HBoxContainer.new() - team_row.add_theme_constant_override("separation", UIScale.px(6)) - spawn_panel.add_child(team_row) - var team_group := ButtonGroup.new() - spawn_team_blue = _spawn_team_button("🔵 Team Blau", team_group, 0) - spawn_team_red = _spawn_team_button("🔴 Team Rot", team_group, 1) - spawn_team_blue.button_pressed = true - team_row.add_child(spawn_team_blue) - team_row.add_child(spawn_team_red) - - spawn_radius_label = _field_label("Zonen-Radius: 6 m") - spawn_panel.add_child(spawn_radius_label) - spawn_radius_slider = HSlider.new() - spawn_radius_slider.min_value = SpawnOps.MIN_ZONE_RADIUS - spawn_radius_slider.max_value = SpawnOps.MAX_ZONE_RADIUS - spawn_radius_slider.step = 1.0 - spawn_radius_slider.value = SpawnOps.DEFAULT_ZONE_RADIUS - spawn_radius_slider.custom_minimum_size = UIScale.v2(0, 32) - spawn_radius_slider.value_changed.connect(_on_spawn_radius_changed) - spawn_panel.add_child(spawn_radius_slider) - - spawn_sel_label = Label.new() - spawn_sel_label.text = "Keine Zone ausgewählt." - spawn_sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - spawn_panel.add_child(spawn_sel_label) - spawn_delete_btn = Button.new() - spawn_delete_btn.text = "Zone löschen (Entf)" - spawn_delete_btn.custom_minimum_size = UIScale.v2(0, 40) - spawn_delete_btn.tooltip_text = "Die ausgewählte Spawn-Zone löschen (Strg+Z holt sie zurück)" - spawn_delete_btn.pressed.connect(_on_spawn_delete_pressed) - spawn_panel.add_child(spawn_delete_btn) - - spawn_panel.add_child(_muted_label( - "Klick setzt eine Zone, Klick+Ziehen verschiebt sie. Jede Zone ist " - + "EIN Rückgängig-Schritt (Strg+Z). ESC beendet das Spawn-Werkzeug.")) - - # --- Bauen (Karte t_97ea59c5): Modular-Builder-Pieces im App-Skelett --- - vbox.add_child(_separator()) - build_edit_btn = Button.new() - build_edit_btn.text = "🏠 Bauen (Häuser)" - build_edit_btn.custom_minimum_size = UIScale.v2(0, 48) - build_edit_btn.toggle_mode = true - build_edit_btn.tooltip_text = "Fundamente, Wände, Dächer, Treppen und Türen bauen — im 2-Meter-Raster, wie Bauklötze" - build_edit_btn.pressed.connect(_on_build_btn_pressed) - vbox.add_child(build_edit_btn) - - build_panel = VBoxContainer.new() - build_panel.add_theme_constant_override("separation", UIScale.px(6)) - build_panel.visible = false - vbox.add_child(build_panel) - - build_panel.add_child(_field_label("Bauteil:")) - var build_grid := GridContainer.new() - build_grid.columns = 2 - build_grid.add_theme_constant_override("h_separation", UIScale.px(6)) - build_grid.add_theme_constant_override("v_separation", UIScale.px(6)) - build_panel.add_child(build_grid) - var build_group := ButtonGroup.new() - for kind_id in BuildOps.KINDS.keys(): - var kind_btn := Button.new() - kind_btn.text = str(BuildOps.KINDS[kind_id]["label"]) - kind_btn.toggle_mode = true - kind_btn.button_group = build_group - kind_btn.custom_minimum_size = UIScale.v2(148, 44) - kind_btn.clip_text = true - kind_btn.tooltip_text = "%s — %s" % [str(BuildOps.KINDS[kind_id]["label"]), str(BuildOps.KINDS[kind_id].get("info", ""))] - kind_btn.pressed.connect(_on_build_kind_pressed.bind(str(kind_id))) - build_kind_buttons[str(kind_id)] = kind_btn - build_grid.add_child(kind_btn) - (build_kind_buttons["fundament"] as Button).set_pressed_no_signal(true) - - build_panel.add_child(_muted_label( - "Linksklick setzt das Bauteil aufs 2-m-Raster — auch AUF andere Teile " - + "(Wände aufs Fundament stapeln, Dach auf die Wände). Jedes Bauteil ist " - + "EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Bauen-Modus.")) - - # --- Test-Startpunkt (Karte t_96e3c4ee): Info + Löschen im Werkzeug --- - test_start_panel = VBoxContainer.new() - test_start_panel.add_theme_constant_override("separation", UIScale.px(6)) - test_start_panel.visible = false - vbox.add_child(test_start_panel) - - test_start_info = Label.new() - test_start_info.text = "" - test_start_info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - test_start_panel.add_child(test_start_info) - test_start_delete_btn = Button.new() - test_start_delete_btn.text = "🗑 Test-Start löschen" - test_start_delete_btn.custom_minimum_size = UIScale.v2(0, 40) - test_start_delete_btn.tooltip_text = "Test-Startpunkt entfernen — F5 startet dann wieder am ersten Team-Spawnpunkt (sonst Kartenmitte). Entf geht auch." - test_start_delete_btn.pressed.connect(_on_test_start_delete_pressed) - test_start_panel.add_child(test_start_delete_btn) - test_start_panel.add_child(_muted_label( - "Der Test-Start ist KEIN Spiel-Spawnpunkt: er landet nur in einer " - + "Editor-Datei, nicht in der Map — Spawn-Checks und Analysen bleiben unberührt.")) - - # Signale (Änderungen → Dokument + Ansicht) - prop_name.text_changed.connect(_on_prop_changed.bind("name")) - prop_author.text_changed.connect(_on_prop_changed.bind("author")) - prop_desc.text_changed.connect(_on_prop_changed.bind("description")) - prop_version.value_changed.connect(_on_version_changed) - prop_weather.item_selected.connect(_on_weather_changed) - return panel - - -func _sel_op_button(text: String, tip: String, action: Callable) -> Button: - var btn := Button.new() - btn.text = text - btn.custom_minimum_size = UIScale.v2(140, 40) - btn.tooltip_text = tip - btn.pressed.connect(action) - return btn - - -func _header_label(text: String) -> Label: - var label := Label.new() - label.text = text - label.add_theme_font_size_override("font_size", UIScale.f(19)) - label.add_theme_color_override("font_color", Color(0.55, 0.8, 1.0)) - return label - - -func _field_label(text: String) -> Label: - var label := Label.new() - label.text = text - label.modulate = Color(1, 1, 1, 0.7) - return label - - -func _muted_label(text: String) -> Label: - var label := Label.new() - label.text = text - label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - label.modulate = Color(1, 1, 1, 0.55) - return label - - -func _separator() -> HSeparator: - return HSeparator.new() - - # ------------------------------------------------------------ Map-Aktionen -- func _start_with_new_map() -> void: @@ -1460,11 +703,6 @@ func _rebuild_ui_for_scale() -> void: if str(_update_result.get("status")) == "update_available": _show_update_banner(str(_update_result.get("local", "")), str(_update_result.get("remote", ""))) _sync_size_menu() - # Karte t_96e3c4ee: Werkzeug + Marker für den Test-Start neu aufsetzen - # (das Rebuild hat einen frischen Tool-Node ohne Marker angelegt). - if test_start_tool != null: - test_start_tool.call("refresh_from_doc") - _sync_test_start_panel() ## Fenster beim Start: maximiert (Default, kein Vollbild-Zwang) oder die @@ -1512,6 +750,7 @@ func _save_window_state() -> void: ## Ein Ausgang für beide Beenden-Wege (Menü + Fenster-X). func _quit_editor() -> void: _save_window_state() + _save_dock_state() get_tree().quit() @@ -1816,17 +1055,14 @@ func _start_playtest() -> void: playtest.exit_requested.connect(_on_playtest_exit_requested) world3d.add_child(playtest) camera.current = false # Editor-Orbit abgeben — die Spiel-Kamera übernimmt - # Karte t_96e3c4ee: Fallback-Reihenfolge mit Klartext-Hinweis — gesetzter - # Test-Start → erster Team-Spawnpunkt → Kartenmitte. - var resolved: Dictionary = TestStart.resolve_start(doc, test_start_point) - playtest.enter(doc, view_builder, resolved["pos"]) - _set_status("▶ Testlauf läuft: WASD laufen, Shift rennen, Space springen, Maus umsehen, T = zum Blickpunkt versetzen — ESC zurück zum Editor. %s" % resolved["status"]) + playtest.enter(doc, view_builder) + _set_status("▶ Testlauf läuft: WASD laufen, Shift rennen, Space springen, Maus umsehen — ESC zurück zum Editor.") ## Editor-Input einfrieren/freigeben (Kamera-Flug, Werkzeuge, Gizmo). ## Nur auf die Werkzeug-Nodes — die Sicht (view_builder) bleibt 1:1 stehen. func _playtest_set_frozen(on: bool) -> void: - for t in [camera, interaction, terrain_tool, spawn_tool, build_tool, test_start_tool]: + for t in [camera, interaction, terrain_tool, spawn_tool, build_tool]: if t == null: continue t.set_process_unhandled_input(not on) @@ -1861,7 +1097,6 @@ func _on_new_map_confirmed() -> void: history.begin(doc) _clear_edit_state() _refresh_all() - _test_start_reload() # Karte t_96e3c4ee: neue Map → kein Test-Start gesetzt _set_status("Neue Map „%s“ erstellt (%.0f × %.0f m, Wetter: %s). Wähle links ein Objekt, um zu bauen!" % [ new_dialog.get_map_name(), new_dialog.get_size_m(), new_dialog.get_size_m(), new_dialog.get_weather()]) @@ -1878,7 +1113,6 @@ func _on_open_selected(path: String) -> void: history.begin(doc) _clear_edit_state() _refresh_all() - _test_start_reload() # Karte t_96e3c4ee: Sidecar der geladenen Map lesen var map_name := str(doc.get("info", {}).get("name", "?")) _set_status("Map „%s“ geladen (%d Objekte). Klicke ein Objekt in der Karte an, um es anzupassen." % [map_name, SvmapDocument.object_count(doc)]) @@ -1916,7 +1150,6 @@ func _save_to(path: String) -> void: return current_path = path dirty = false - _test_start_reload() # Karte t_96e3c4ee: Punkt wandert beim Speichern mit _refresh_file_label() _set_status("Map gespeichert: %s" % path) _refresh_tree() @@ -1987,7 +1220,6 @@ func _update_tool_label() -> void: "terrain": "Terrain (F1)", "bauen": "Bauen (F3)", "spawns": "Spawn-Zonen (F4)", - "teststart": "Test-Start (T)", } tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode)) @@ -2068,20 +1300,12 @@ func _unhandled_key_input(event: InputEvent) -> void: # bei jeder Taste, GUI-Start-Befund 15.10.). Die Godot-4-Ersatz-API ist # is_command_or_control_pressed(): Ctrl auf Linux/Windows, Cmd auf macOS. var ctrl: bool = key_evt.is_command_or_control_pressed() - # Lauf-Modus (t_1cdc1a72): nur ESC + T behandeln — ALLE anderen Editor- - # Tasten (F1-F5, Undo/Redo, Werkzeuge) sind im Spiel eingefroren. + # Lauf-Modus (t_1cdc1a72): nur ESC behandeln — ALLE anderen Editor-Tasten + # (F1-F5, Undo/Redo, Werkzeuge) sind im Spiel eingefroren. if playtest != null and playtest.active: if key == KEY_ESCAPE: playtest.handle_escape() get_viewport().set_input_as_handled() - elif key == KEY_T: - # Karte t_96e3c4ee, „jederzeit“: T im Testlauf = Test-Start auf den - # Punkt unterm Fadenkreuz setzen + SOFORT dorthin versetzen - # (Augenhöhe 1,70 m, Kollision geprüft). Speichern + Marker: unten. - var tp: Dictionary = playtest.call("teleport_crosshair") - if bool(tp.get("ok")): - _test_start_apply(float(tp.get("x")), float(tp.get("z")), "") - get_viewport().set_input_as_handled() return match key: KEY_Z: @@ -2131,12 +1355,6 @@ func _unhandled_key_input(event: InputEvent) -> void: # (Karte t_b84af40f) — F5 ruft ihn direkt auf. _on_test_run() get_viewport().set_input_as_handled() - KEY_T: - # Karte t_96e3c4ee: T = Test-Start-Werkzeug an/aus (Toggle, wie F1–F4). - # Nur ohne Modifier + gleiche Freigabe wie F1–F5/H. - if not ctrl and _tool_key_allowed(): - _set_tool_mode("objects" if tool_mode == "teststart" else "teststart") - get_viewport().set_input_as_handled() KEY_H: # Karte 3/3: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls). if _tool_key_allowed(): @@ -2202,14 +1420,6 @@ func _unhandled_key_input(event: InputEvent) -> void: if gizmo != null and gizmo.cancel_drag_if_active(): get_viewport().set_input_as_handled() return - # Karte t_96e3c4ee: Das Test-Start-Werkzeug hat keinen ShortcutPolicy- - # Eintrag (Datei-Grenze der Karte) — Esc verlässt es direkt. Der - # gesetzte Punkt bleibt dabei erhalten (nur das Werkzeug endet). - if tool_mode == "teststart": - _set_tool_mode("objects") - _set_status("Test-Start-Werkzeug beendet — der gesetzte Punkt bleibt gesetzt (F5 startet dort).") - get_viewport().set_input_as_handled() - return # Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar): # Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview # abbrechen → Auswahl aufheben (Greifen: Karte t_55257792). @@ -2245,10 +1455,6 @@ func _unhandled_key_input(event: InputEvent) -> void: spawn_tool.call("delete_selected") elif tool_mode == "bauen": build_tool.call("delete_selected") - elif tool_mode == "teststart": - # Karte t_96e3c4ee: Entf im Test-Start-Werkzeug = Punkt löschen. - _test_start_delete() - get_viewport().set_input_as_handled() else: interaction.delete_selected() get_viewport().set_input_as_handled() @@ -2323,27 +1529,30 @@ func _unhandled_key_input(event: InputEvent) -> void: # --------------------------------------------------------- Bibliothek ------ +# U1: die Bibliothek wohnt im Assets-Bereich unten (assets_panel.gd); die +# Tools rufen weiter main.call("_highlight_library") — dünne Weiterleitung. func _on_library_pressed(oid: String) -> void: - if tool_mode != "objects": - _set_tool_mode("objects") # Bibliothek-Klick wechselt automatisch zurück - if interaction.placing and interaction.place_oid == oid: - # Nochmal geklickt → Modus beenden (An/Aus ohne Umwege). - interaction.cancel_placement() - _set_status("Platzieren abgebrochen.") - return - for other_oid in library_buttons: - (library_buttons[other_oid] as Button).button_pressed = false - library_buttons[oid].button_pressed = true - interaction.start_placement(oid) + assets_mod._on_library_pressed(oid) + + +## U1-Delegation: die Bibliotheks-Logik wohnt in assets_panel.gd — die alten +## main.-Aufrufe (Werkzeuge/Tests) bleiben als dünne Delegaten erhalten. +func _apply_library_filter(query: String) -> void: + assets_mod._apply_library_filter(query) + + +func _on_library_expand_all() -> void: + assets_mod._on_library_expand_all() + + +func _on_library_collapse_all() -> void: + assets_mod._on_library_collapse_all() ## Der Bibliothek-Button des aktiven Platzier-Objekts leuchtet auf. func _highlight_library(oid: String) -> void: - for other_oid in library_buttons: - var btn := library_buttons[other_oid] as Button - btn.button_pressed = other_oid == oid - btn.modulate = Color(1, 1, 1) if oid == other_oid else Color(1, 1, 1, 0.85) + assets_mod.highlight_library(oid) func object_label(oid: String) -> String: @@ -2385,17 +1594,15 @@ func _base_tool_status() -> void: # ------------------------------------------------------ Terrain-Werkzeug ---- # (Karte t_9aba04f1 — Höhenfeld heben/senken/glätten) -func _terrain_mode_button(text: String, group: ButtonGroup) -> Button: - var btn := Button.new() - btn.text = text - btn.toggle_mode = true - btn.button_group = group - btn.custom_minimum_size = UIScale.v2(0, 40) - return btn - +## U1-Delegation: die Widgets wohnen in den Modulen (terrain_panel.gd, +## check_panel.gd); der Koordinator hält die Werkzeug-Logik und steuert die +## Modul-Widgets über deren öffentliche Felder. Methoden, die die Tools per +## main.call(...) erreichen (_on_terrain_stroke_committed, _sync_spawn_selection, +## _refresh_spawn_status, _mark_tree_selection…), bleiben unter denselben +## Namen hier (dünne Weiterleitung) — der Werkzeug-Vertrag ist unverändert. func _on_terrain_btn_pressed() -> void: - if terrain_edit_btn.button_pressed: + if terrain_mod.terrain_edit_btn.button_pressed: _set_tool_mode("terrain") else: _set_tool_mode("objects") @@ -2408,34 +1615,24 @@ func _set_tool_mode(mode: String) -> void: _sync_terrain_button() _sync_spawn_button() _sync_build_button() - _sync_test_start_button() _update_tool_label() return if mode == "terrain": _leave_spawn_mode() _leave_build_mode() - _leave_test_start_mode() _enter_terrain_mode() elif mode == "spawns": _leave_terrain_mode() _leave_build_mode() - _leave_test_start_mode() _enter_spawn_mode() elif mode == "bauen": _leave_terrain_mode() _leave_spawn_mode() - _leave_test_start_mode() _enter_build_mode() - elif mode == "teststart": - _leave_terrain_mode() - _leave_spawn_mode() - _leave_build_mode() - _enter_test_start_mode() else: _leave_terrain_mode() _leave_spawn_mode() _leave_build_mode() - _leave_test_start_mode() _enter_objects_mode() _update_tool_label() # Karte t_89c0ec95: Werkzeugwechsel = Höhenlinien-Umschalter — nach dem @@ -2453,18 +1650,18 @@ func _enter_terrain_mode() -> void: _sync_terrain_button() return tool_mode = "terrain" - terrain_panel.visible = true + terrain_mod.terrain_panel.visible = true help_label.text = "Linksklick-Ziehen: Boden formen oder malen · Strg+Linksklick: Gegenteil / Gras zurück ·\n1/2/3/4: Heben/Senken/Glätten/Malen · [ / ]: Pinselgröße Klein/Mittel/Groß ·\nPinselgröße & Material rechts · Rechte Maustaste: Flug · Strg+Z: letzter Strich weg" - terrain_edit_btn.button_pressed = true - terrain_tool.call("set_brush_mode", _selected_terrain_mode()) + terrain_mod.terrain_edit_btn.button_pressed = true + terrain_tool.call("set_brush_mode", terrain_mod._selected_terrain_mode()) _set_status("Terrain-Modus an: LINKSKLICK-Ziehen formt den Boden, im Malen-Modus färbt er das Gelände (Gras/Sand/Fels). Regler rechts stellen Pinsel und Stärke ein.") func _leave_terrain_mode() -> void: if terrain_tool != null: terrain_tool.call("deactivate") - if terrain_panel != null: - terrain_panel.visible = false + if terrain_mod != null: + terrain_mod.terrain_panel.visible = false _sync_terrain_button() @@ -2472,21 +1669,21 @@ func _enter_spawn_mode() -> void: interaction.call("cancel_placement") interaction.call("clear_selection", true) tool_mode = "spawns" - spawn_panel.visible = true + check_mod.spawn_panel.visible = true help_label.text = "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Entf: löschen ·\nTeam/Radius rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück" - spawn_edit_btn.button_pressed = true + check_mod.spawn_edit_btn.button_pressed = true spawn_tool.call("activate") - spawn_tool.call("set_team", _selected_spawn_team()) - spawn_tool.call("set_radius", float(spawn_radius_slider.value)) + spawn_tool.call("set_team", check_mod._selected_spawn_team()) + spawn_tool.call("set_radius", float(check_mod.spawn_radius_slider.value)) _refresh_spawn_status() - _set_status("Spawn-Modus an: Klicke auf den Boden, um Zonen für %s zu malen. Die Kreis-Vorschau folgt der Maus." % SpawnOps.team_label(_selected_spawn_team())) + _set_status("Spawn-Modus an: Klicke auf den Boden, um Zonen für %s zu malen. Die Kreis-Vorschau folgt der Maus." % SpawnOps.team_label(check_mod._selected_spawn_team())) func _leave_spawn_mode() -> void: if spawn_tool != null: spawn_tool.call("deactivate") - if spawn_panel != null: - spawn_panel.visible = false + if check_mod != null: + check_mod.spawn_panel.visible = false _sync_spawn_button() @@ -2499,6 +1696,10 @@ func _enter_objects_mode() -> void: # ------------------------------------------------------ Bauen-Werkzeug ------ # (Karte t_97ea59c5 — Modular-Builder-Pieces direkt im App-Skelett; die # Godot-Editor-UI des Plugins bleibt unangetastet im Addon aktivierbar.) +# U1: die Bauteil-Widgets wohnen im Terrain-Tab (terrain_panel.gd, Werkzeug- +# Zusammenhang); die Werkzeug-Logik bleibt hier im Koordinator. Tests/Tools +# erreichen sie über main.build_edit_btn/main.build_kind_buttons wie vorher. + func _on_build_btn_pressed() -> void: if build_edit_btn.button_pressed: @@ -2539,177 +1740,26 @@ func _sync_build_button() -> void: build_edit_btn.button_pressed = tool_mode == "bauen" -# ---------------------------------------------------- Test-Startpunkt ------- -# (Karte t_96e3c4ee — „hinklicken, wo man beim Testen spawnt“. Editor-seitig, -# Sidecar statt .svmap, KEIN Spiel-Spawnpunkt: kein Undo, keine Spawn-Checks.) - -func _on_test_start_btn_pressed() -> void: - if test_start_btn.button_pressed: - _set_tool_mode("teststart") - else: - _set_tool_mode("objects") - - -func _enter_test_start_mode() -> void: - interaction.call("cancel_placement") - interaction.call("clear_selection", true) - tool_mode = "teststart" - test_start_panel.visible = true - help_label.text = "Linksklick: Test-Start setzen (Höhe automatisch aufs Terrain) · Auf Objekt/Bauteil: Boden darunter\nEntf: Test-Start löschen · Mittlere Maustaste: Kamera · ESC: Werkzeug beenden" - test_start_btn.button_pressed = true - test_start_tool.call("activate") - _sync_test_start_panel() - _set_status("Test-Start-Werkzeug an: Klicke in die Karte, wo der Testlauf (F5) starten soll. Der Punkt ist KEIN Spiel-Spawnpunkt — er landet nur in einer Editor-Datei, nicht in der Map.") - - -func _leave_test_start_mode() -> void: - if test_start_tool != null: - test_start_tool.call("deactivate") - if test_start_panel != null: - test_start_panel.visible = false - _sync_test_start_button() - - -func _sync_test_start_button() -> void: - test_start_btn.button_pressed = tool_mode == "teststart" - - -## Panel-Zeile rechts (Info + Löschen-Knopf) mit dem Punkt-Zustand syncen. -func _sync_test_start_panel() -> void: - if test_start_info == null or test_start_delete_btn == null: - return - if test_start_point == null: - test_start_info.text = "Kein Test-Start gesetzt.\nF5 startet dann am ersten Team-Spawnpunkt (sonst Kartenmitte)." - test_start_delete_btn.disabled = true - else: - var p := test_start_point as Vector3 - test_start_info.text = "Test-Start: x=%.0f z=%.0f\n(Höhe kommt automatisch vom Terrain)" % [p.x, p.z] - test_start_delete_btn.disabled = false - - -## Klick (oder Taste T im Testlauf): Test-Start setzen. NUR x/z — die Höhe -## kommt immer automatisch vom Terrain (Marker holt sie je nach Stand). -## Kein History-Eintrag, kein doc-Eintrag, kein Spawn-Check-Kontakt. -func _test_start_apply(x: float, z: float, hinweis: String) -> void: - test_start_point = Vector3(x, 0.0, z) - TestStartStore.save_point(current_path, test_start_point) - test_start_tool.call("refresh_from_doc") - _sync_test_start_panel() - var ort := " (Höhe automatisch)" if hinweis.is_empty() else " (Höhe automatisch — %s)" % hinweis - _set_status("Test-Start: x=%.0f z=%.0f%s — F5 startet den Testlauf genau dort." % [x, z, ort]) - - -## Test-Start löschen (Entf im Werkzeug oder Knopf rechts). -func _on_test_start_delete_pressed() -> void: - _test_start_delete() - - -func _test_start_delete() -> void: - if test_start_point == null: - _set_status("Es ist kein Test-Startpunkt gesetzt — klicke zuerst mit dem Test-Start-Werkzeug (T) in die Karte.", true) - return - test_start_point = null - TestStartStore.clear_point(current_path) - test_start_tool.call("refresh_from_doc") - _sync_test_start_panel() - _set_status("Test-Start gelöscht — F5 startet wieder am ersten Team-Spawnpunkt (sonst Kartenmitte).") - - -## Nach Neu/Laden/Speichern (Pfadwechsel → anderer Sidecar-Schlüssel): den -## gespeicherten Punkt für DIESE Map wiederherstellen. Eine Map ohne Punkt -## verhält sich exakt wie bisher. -func _test_start_reload() -> void: - test_start_point = TestStartStore.load_point(current_path) - test_start_tool.call("refresh_from_doc") - _sync_test_start_panel() - - -func _selected_terrain_mode() -> int: - if terrain_mode_lower.button_pressed: - return TerrainField.Mode.LOWER - if terrain_mode_smooth.button_pressed: - return TerrainField.Mode.SMOOTH - if terrain_mode_paint.button_pressed: - return TerrainField.Mode.PAINT - return TerrainField.Mode.RAISE - - func _sync_terrain_button() -> void: - terrain_edit_btn.button_pressed = tool_mode == "terrain" + terrain_mod.terrain_edit_btn.button_pressed = tool_mode == "terrain" -## Mode-Buttons (ButtonGroup) → Pinsel-Modus; Palette nur im Malen-Modus. -func _on_terrain_mode_changed(_pressed: bool) -> void: - if terrain_palette != null: - terrain_palette.visible = _selected_terrain_mode() == TerrainField.Mode.PAINT - if terrain_tool != null: - terrain_tool.call("set_brush_mode", _selected_terrain_mode()) - - -## Material-Palette (Karte t_603ec1a8): gewähltes Material an den Pinsel. -func _on_terrain_material_changed(pressed: bool, mat_id: int) -> void: - if pressed and terrain_tool != null: - terrain_tool.call("set_paint_material", mat_id) - - -## Pinselgrößen-Presets (Karte t_603ec1a8): setzen den Radius-Slider -## (der Label-Text + Ring-Optik hängen am value_changed-Signal). -func _terrain_size_button(text: String, group: ButtonGroup, radius_m: float) -> Button: - var btn := Button.new() - btn.text = text - btn.toggle_mode = true - btn.button_group = group - btn.custom_minimum_size = UIScale.v2(0, 48) - btn.tooltip_text = "Pinselradius %d m" % int(radius_m) - btn.toggled.connect(_on_terrain_size_preset.bind(radius_m)) - return btn - - -func _on_terrain_size_preset(pressed: bool, radius_m: float) -> void: - # Null-Check als Schutznetz: beim Panel-Bau existiert der Slider noch - # nicht (Programmatic-Pressed darf den Handler nie früh treffen). - if pressed and terrain_radius_slider != null: - terrain_radius_slider.value = radius_m - - -## Terrain-Pinsel-Größe per [ / ] (Karte t_55257792): durch die 3 Presets -## Klein/Mittel/Groß zyklisch — der Preset-Button wird mit gesetzt (set_pressed -## feuert toggled → Slider + Label + Pinssel-Ring wandern mit). func _terrain_brush_size_step(up: bool) -> void: - if terrain_radius_slider == null: - return - var next := MoveMath.brush_next_preset(float(terrain_radius_slider.value), up) - var btn: Button = null - match next: - 4.0: - btn = terrain_size_btn_small - 12.0: - btn = terrain_size_btn_medium - 32.0: - btn = terrain_size_btn_large - if btn != null: - btn.set_pressed(true) # feuert _on_terrain_size_preset → Slider/Label - else: - terrain_radius_slider.value = next - terrain_tool.call("set_radius", next) - _set_status("Pinsel-Größe: %d m (mit [ / ] durch Klein/Mittel/Groß zyklisch)." % int(next)) + terrain_mod.terrain_brush_size_step(up) -## Terrain-Pinsel-Modus per Ziffer 1–4 (Karte t_55257792): Heben/Senken/ -## Glätten/Malen — der Modus-Button wird mit gesetzt (toggled feuert die -## bestehende Sync-Kette inkl. Material-Palette im Malen-Modus). Malen startet -## mit der zuletzt gewählten Materialfarbe (paint_material bleibt stehen). func _terrain_brush_mode_key(mode_idx: int) -> void: + var t: Control = terrain_mod var btn: Button = null match mode_idx: 0: - btn = terrain_mode_raise + btn = t.terrain_mode_raise 1: - btn = terrain_mode_lower + btn = t.terrain_mode_lower 2: - btn = terrain_mode_smooth + btn = t.terrain_mode_smooth 3: - btn = terrain_mode_paint + btn = t.terrain_mode_paint if btn == null or terrain_tool == null: return btn.set_pressed(true) @@ -2721,18 +1771,6 @@ func _terrain_brush_mode_key(mode_idx: int) -> void: _set_status("Pinsel-Modus: %s%s — Linksklick-Ziehen formt/malt jetzt so (Ziffern 1–4 wechseln)." % [str(labels[mode_idx]), extra]) -func _on_terrain_radius_changed(value: float) -> void: - terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value) - if terrain_tool != null: - terrain_tool.call("set_radius", value) - - -func _on_terrain_strength_changed(value: float) -> void: - terrain_strength_label.text = "Stärke: %.1f m pro Sekunde" % value - if terrain_tool != null: - terrain_tool.call("set_strength", value) - - ## Ein Pinsel-Strich ist fertig (Doc + History schon aktualisiert vom Tool). func _on_terrain_stroke_committed() -> void: dirty = true @@ -2752,7 +1790,7 @@ func _recheck_terrain_mode() -> void: if not terrain_tool.call("refresh_from_doc"): terrain_tool.call("deactivate") tool_mode = "objects" - terrain_panel.visible = false + terrain_mod.terrain_panel.visible = false _sync_terrain_button() _update_tool_label() # Karte t_89c0ec95: Dezenter Terrain-Rückschalter = ebenfalls ein @@ -2763,69 +1801,23 @@ func _recheck_terrain_mode() -> void: # ---------------------------------------------------- Spawn-Zonen-Werkzeug --- -# (Karte t_c4162c5d — Spawn-Zonen malen, Live-Check, Map-Fertig-Check) - -func _spawn_team_button(text: String, group: ButtonGroup, team: int) -> Button: - var btn := Button.new() - btn.text = text - btn.toggle_mode = true - btn.button_group = group - btn.custom_minimum_size = UIScale.v2(0, 40) - btn.pressed.connect(_on_spawn_team_changed.bind(team)) - return btn - func _on_spawn_btn_pressed() -> void: - if spawn_edit_btn.button_pressed: - _set_tool_mode("spawns") - else: - _set_tool_mode("objects") - - -func _selected_spawn_team() -> int: - return 1 if spawn_team_red.button_pressed else 0 + check_mod._on_spawn_btn_pressed() func _sync_spawn_button() -> void: - spawn_edit_btn.button_pressed = tool_mode == "spawns" - - -func _on_spawn_team_changed(team: int) -> void: - if spawn_tool != null and tool_mode == "spawns": - spawn_tool.call("set_team", team) - - -func _on_spawn_radius_changed(value: float) -> void: - spawn_radius_label.text = "Zonen-Radius: %d m" % int(value) - if spawn_tool != null and tool_mode == "spawns": - spawn_tool.call("set_radius", value) - - -func _on_spawn_delete_pressed() -> void: - if tool_mode == "spawns": - spawn_tool.call("delete_selected") + check_mod.spawn_edit_btn.button_pressed = tool_mode == "spawns" ## Panel-Zeile „Welche Zone ist angewählt?“ — WIRD vom Spawn-Tool gerufen. func _sync_spawn_selection() -> void: - if spawn_tool == null: - return - var team: int = spawn_tool.get("selected_team") - var pid: String = str(spawn_tool.get("selected_id")) - if team < 0 or pid == "": - spawn_sel_label.text = "Keine Zone ausgewählt.\nKlicke im Viewport auf einen Kreis." - spawn_delete_btn.disabled = true - return - spawn_sel_label.text = "Ausgewählt: %s (%s)\nVerschieben: klicken+ziehen · Radius: Regler oben" % [ - pid, SpawnOps.team_label(team)] - spawn_delete_btn.disabled = false + check_mod.sync_spawn_selection() ## LIVE-Validierung (Loader-Regeln) — immer sichtbar, nach JEDEM Struktur-Wechsel. func _refresh_spawn_status() -> void: - if doc.is_empty() or spawn_status == null: - return - spawn_status.text = SpawnOps.live_status(doc, manifest) + check_mod.refresh_spawn_status() ## Map-Fertig-Check (Export-Button-Aktion): Klartext-Liste + Shake, wenn fehlt. @@ -2842,7 +1834,7 @@ func _on_check_map() -> void: if report["ok"]: _set_status("✅ Karte prüfen: Alles bereit zum Export! Speichern erzeugt die signierte .svmap-Datei.") else: - _shake_button(check_btn) + _shake_button(_registry_toolbar_button("karte_pruefen")) _set_status("Map ist noch nicht bereit — siehe Warnungen im Map-Check-Fenster.", true) @@ -2854,229 +1846,15 @@ func _shake_button(btn: Control) -> void: tween.tween_property(btn, "position", orig + Vector2(offset, 0.0), 0.045) -## Der Baum ruft zurück, wenn der Nutzer dort ein Objekt auswählt. -## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl, -## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln -## sich in die Viewport-Auswahl (Baum und Szene sind immer synchron). -func _on_tree_selected() -> void: - if _tree_select_lock: - return - var item := content_tree.get_selected() - if item == null: - return - if item.has_meta("gebaeude_index"): - _on_tree_selected_gebaeude(int(item.get_meta("gebaeude_index"))) - return - var indices: Array = [] - var walk := content_tree.get_next_selected(null) - while walk != null: - if walk.has_meta("obj_index"): - indices.append(int(walk.get_meta("obj_index"))) - walk = content_tree.get_next_selected(walk) - if indices.is_empty(): - return - var objects: Array = doc.get("objects", []) - var valid: Array = [] - for idx in indices: - if idx >= 0 and idx < objects.size(): - valid.append(idx) - if valid.is_empty(): - return - if valid.size() == 1: - interaction.select_object(valid[0], true) - _set_status("%s ausgewählt — ziehen zum Verschieben, R/Mausrad zum Drehen, Entf zum Löschen. Strg+Klick markiert weitere dazu." % object_label(str(objects[valid[0]].get("id", "")))) - else: - interaction.select_many(valid) - _set_status("%d Objekte im Baum markiert — Entf löscht ALLE, Strg+D dupliziert ALLE (jeweils EIN Undo-Schritt)." % valid.size()) - - -## Baum: Bauteil-Klick → Bauen-Modus + Teil anwählen (Karte t_97ea59c5). -func _on_tree_selected_gebaeude(gi: int) -> void: - if tool_mode != "bauen": - _set_tool_mode("bauen") - build_tool.call("select_index", gi) - - -## Auswahl (aus dem Viewport) im Baum mitmarkieren — MEHRFACH-fähig -## (Karte t_c46d31c2): ALLE gewählten Objekt-Zeilen werden selektiert, Zeilen, -## die nicht mehr gewählt sind, werden abgewählt. Primär-Objekt bleibt sichtbar. -func _mark_tree_selection(idx: int) -> void: - _mark_tree_selection_multi([idx]) - - -func _mark_tree_selection_multi(indices: Array) -> void: - _tree_select_lock = true - var wanted := {} - for i in indices: - wanted[int(i)] = true - _mark_tree_selection_walk(content_tree.get_root(), wanted) - var primary: Variant = interaction.get("selected_index") if interaction != null else -1 - if primary is int and primary >= 0: - _ensure_tree_visible(content_tree.get_root(), primary) - _tree_select_lock = false - - -func _mark_tree_selection_walk(item: TreeItem, wanted: Dictionary) -> void: - if item == null: - return - if item.has_meta("obj_index"): - var idx := int(item.get_meta("obj_index")) - if wanted.has(idx): - item.select(0) - elif item.is_selected(0): - item.deselect(0) - for child in item.get_children(): - _mark_tree_selection_walk(child, wanted) - - -func _ensure_tree_visible(item: TreeItem, idx: int) -> void: - if item == null: - return - if item.has_meta("obj_index") and int(item.get_meta("obj_index")) == idx: - # Fix (beim Inspektor-Einbau gefunden, Vorbestand t_f6f29d86): Tree - # und TreeItem haben KEIN ensure_visible — die Godot-4-API heißt - # Tree.scroll_to_item. Der alte Ruf warf bei JEDER Auswahl einen - # SCRIPT ERROR und scrollte den Baum nie zur Auswahl. - content_tree.scroll_to_item(item) - return - for child in item.get_children(): - _ensure_tree_visible(child, idx) - - -## Rechte Panel-Sektion synchronisieren OHNE Statuszeilen-Text zu ändern -## (sonst überschreibt ein Stummsync die gerade gesetzte Hilfs-Meldung). -## Mehrfachauswahl/Gruppen-fähig (Karte t_f6f29d86): Panel listet ALLE -## gewählten Objekte auf + zeigt die Gruppen-Bindung an. -func _sync_selection_panel() -> void: - # Objekt-Inspektor mit allen Auswahl-Wege synchron halten (Karte t_bb0d11c9). - if inspector != null: - inspector.call("refresh") - # Gizmo-Schrittweite-Feld mitziehen (Karte t_e0120dc2): Das Mausrad - # WÄHREND eines Zugs kann sie im Gizmo ändern (cycle_step) — das Panel- - # Feld folgt ohne eigene value_changed-Rückkopplung (set_value_no_signal). - if gizmo_step_box != null and is_instance_valid(gizmo_step_box) \ - and not gizmo_step_box.has_focus(): - gizmo_step_box.set_value_no_signal(gizmo_step_m) - var objects: Array = doc.get("objects", []) - var sel: Array = interaction.selected_indices - var valid: Array = [] - for i in sel: - if i is int and i >= 0 and i < objects.size(): - valid.append(i) - var has_sel := not valid.is_empty() - for btn in [sel_rotate_btn, sel_delete_btn, sel_dup_btn, sel_mirror_x_btn, - sel_mirror_z_btn, sel_raise_btn, sel_lower_btn]: - btn.disabled = not has_sel - var has_group := false - for i in valid: - if GroupOps.group_id(objects[i]) != GroupOps.UNGROUPED: - has_group = true - break - sel_group_btn.disabled = valid.size() < 2 - sel_ungroup_btn.disabled = not has_group - if not has_sel: - sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an." - return - if valid.size() == 1: - var idx: int = valid[0] - var obj: Dictionary = objects[idx] - var pos: Array = obj.get("pos", [0.0, 0.0, 0.0]) - var lift := ObjectOps.lift_offset(doc, idx) - var lift_text := "am Boden" if absf(lift) < 0.02 else ("%+.2f m über dem Boden" % lift) - var group_line := "" - var gid := GroupOps.group_id(obj) - if gid != GroupOps.UNGROUPED: - group_line = "\nGruppe %d — zieht & dreht GEMEINSAM (Lösen: Button unten)" % (gid + 1) - sel_label.text = "%s (Objekt %d von %d)\nPosition: %.0f, %.0f m · Höhe: %s · Richtung: %.0f°%s\nZiehen: verschieben · R oder Mausrad: drehen (15°)\nBild↑/↓: anheben/absenken · Strg+D: duplizieren\nSpiegeln/Löschen: Buttons oben · Strg+Z: zurück" % [ - object_label(str(obj.get("id", ""))), idx + 1, objects.size(), - float(pos[0]), float(pos[2]), lift_text, float(obj.get("rot_y_deg", 0.0)), group_line] - return - # Mehrfachauswahl: Liste aller gewählten Objekte (max. 8 Zeilen, dann „…“). - var names := PackedStringArray() - for i in valid: - if names.size() < 8: - names.append(object_label(str((objects[i] as Dictionary).get("id", "?")))) - var more := "" - if valid.size() > 8: - more = "\n… und %d weitere" % (valid.size() - 8) - sel_label.text = "%d Objekte ausgewählt:\n%s%s\nGemeinsam: ziehen (Gruppe wandert zusammen), drehen um die Mitte, Entf löscht alle." % [ - valid.size(), " · ".join(names), more] - - -## Hilfs-Meldung zur frischen Auswahl (Klartext + Budget-Warnung + nächster Schritt). -func _selection_status(idx: int) -> void: - # Objekt-Inspektor füllen (Karte t_bb0d11c9) — dieser Weg läuft bei - # 3D-Klick, Rahmen-Ausziehen und Map-Inhalt-Baum. - if inspector != null: - inspector.call("refresh") - # Fix (Vorbestand, beim Inspektor-Einbau gefunden): select_object/ - # select_many füllten das alte „Auswahl“-Feld NIE — sel_label blieb auf - # „Nichts ausgewählt“ stehen, während die Statuszeile das Objekt nannte. - # _sync_selection_panel ändert bewusst KEINEN Statuszeilen-Text (Stummsync). - _sync_selection_panel() - var objects: Array = doc.get("objects", []) - var sel: Array = interaction.selected_indices - if (idx < 0 or idx >= objects.size()) and sel.is_empty(): - _base_tool_status() - return - var used := int(SvmapDocument.poly_usage(doc, manifest)) - var budget := int(SvmapIO.MAX_TOTAL_POLY) - var budget_warning := "" - if used > budget * 80 / 100: - budget_warning = " ⚠ Poly-Budget zu %.0f %% voll — bald passt kein Objekt mehr rein." % (100.0 * used / budget) - if sel.size() > 1: - _set_status("%d Objekte ausgewählt — ziehen bewegt ALLE gemeinsam, R/Mausrad dreht sie um die Mitte, Entf löscht alle, Strg+G gruppiert.%s" % [sel.size(), budget_warning]) - return - if idx < 0 or idx >= objects.size(): - _base_tool_status() - return - var obj: Dictionary = objects[idx] - var gid := GroupOps.group_id(obj) - var group_hint := "" - if gid != GroupOps.UNGROUPED: - group_hint = " (Gruppe %d — die zieht GEMEINSAM)" % (gid + 1) - _set_status("%s%s ausgewählt — ziehen zum Verschieben, R oder Mausrad zum Drehen, Bild↑/↓ anheben/absenken, Strg+D duplizieren, Entf zum Löschen.%s" % [ - object_label(str(obj.get("id", ""))), group_hint, budget_warning]) - - -# ------------------------------------------------------- Eigenschaften ----- - -func _on_prop_changed(text: String, field: String) -> void: - if _loading_ui_values or doc.is_empty(): - return - var info: Dictionary = doc["info"] - match field: - "name": - info["name"] = text - "author": - info["author"] = text - "description": - info["description"] = text - dirty = true - _refresh_file_label() - - -func _on_version_changed(value: float) -> void: - if _loading_ui_values or doc.is_empty(): - return - doc["info"]["version"] = int(value) - dirty = true - _refresh_file_label() - - -func _on_weather_changed(index: int) -> void: - if _loading_ui_values or doc.is_empty(): - return - var weathers := ["mittag", "abend", "regen", "nacht"] - doc["weather"] = weathers[index] - dirty = true - _refresh_file_label() - # Licht/Stimmung im Viewport sofort anpassen - view_builder.call("rebuild", doc, manifest) - _sync_grid_to_builder() - _set_status("Wetter geändert auf „%s“." % weathers[index]) +## Toolbar-Button per Registry-ID finden (karte_pruefen / karte_testen …). +func _registry_toolbar_button(id: String) -> Control: + var found: Array = toolbar_mod.find_children(id, "Button", true, false) + return found[0] if not found.is_empty() else null +## Baum-Rückrufe wohnen jetzt im Modul (src/ui/panels/scene_tree_panel.gd); +## die Tools rufen weiter main.call("_mark_tree_selection…") — dünne +## Weiterleitungen unten (Registry-Koordinator). # ------------------------------------------------------- Struktur-Änderung -- ## Nach undo/redo/platzieren/löschen: Struktur hat sich geändert → Ansicht neu. @@ -3103,26 +1881,11 @@ func _after_structure_change() -> void: if tool_mode == "bauen" and build_tool != null: build_tool.call("refresh_from_doc") - # --------------------------------------------------------------- Anzeige --- func _refresh_all() -> void: _loading_ui_values = true - var info: Dictionary = doc.get("info", {}) - prop_name.text = str(info.get("name", "")) - prop_author.text = str(info.get("author", "")) - prop_desc.text = str(info.get("description", "")) - prop_version.value = float(info.get("version", 1)) - var weathers := ["mittag", "abend", "regen", "nacht"] - var weather := str(doc.get("weather", "mittag")) - prop_weather.select(maxi(0, weathers.find(weather))) - - var terrain: Dictionary = doc.get("terrain", {}) - terrain_info.text = "Größe: %.0f × %.0f m\nAuflösung: %d × %d Höhenpunkte\nHöhenbereich: %.0f bis %.0f m" % [ - float(terrain.get("size_x_m", 0.0)), float(terrain.get("size_z_m", 0.0)), - int(terrain.get("resolution", 0)), - int(terrain.get("resolution", 0)), - float(terrain.get("min_height_m", 0.0)), float(terrain.get("max_height_m", 0.0))] + map_mod.refresh_values() _loading_ui_values = false _refresh_tree() @@ -3200,48 +1963,30 @@ func _sync_contours_to_builder() -> void: contours_btn.set_pressed_no_signal(_contours_on) +# --- Baum / Auswahl-Sync (U1: Module, dünne Weiterleitung) ------------------- + func _refresh_tree() -> void: - content_tree.clear() - var root := content_tree.create_item() - var objects: Array = doc.get("objects", []) + tree_mod.refresh_tree() - var obj_item := content_tree.create_item(root) - obj_item.set_text(0, "Objekte (%d)" % objects.size()) - for i in objects.size(): - var obj = objects[i] - var item := content_tree.create_item(obj_item) - if obj is Dictionary: - item.set_text(0, "%s #%d" % [object_label(str(obj.get("id", "?"))), i + 1]) - item.set_meta("obj_index", i) - else: - item.set_text(0, "ungültig #%d" % (i + 1)) - var zones: Dictionary = doc.get("spawn_zones", {}) - var spawn_item := content_tree.create_item(root) - spawn_item.set_text(0, "Spawn-Zonen") - for team_key in ["0", "1"]: - if not zones.has(team_key): - continue - var zone: Dictionary = zones[team_key] - var team_item := content_tree.create_item(spawn_item) - var team_name := "Team Blau" if team_key == "0" else "Team Rot" - team_item.set_text(0, "%s (%d Punkte)" % [team_name, (zone.get("points", []) as Array).size()]) - for p in (zone.get("points", []) as Array): - if p is Dictionary: - var point_item := content_tree.create_item(team_item) - point_item.set_text(0, "%s (%.0f, %.0f)" % [ - str(p.get("id", "?")), float(p.get("pos", [0, 0, 0])[0]), float(p.get("pos", [0, 0, 0])[2])]) +## Baum-Klicks kommen im Modul an; die Viewport-Seite (interaction.gd) ruft +## weiter main.call("_mark_tree_selection…") — hier weiterleiten. +func _mark_tree_selection(idx: int) -> void: + tree_mod.mark_tree_selection(idx) - # Gebäude (Karte t_97ea59c5): Bauteile im Baum — Klick wählt das Teil an. - var geb_list: Array = BuildOps.pieces(doc) - if not geb_list.is_empty(): - var geb_item := content_tree.create_item(root) - geb_item.set_text(0, "Gebäude (%d Bauteile)" % geb_list.size()) - for i in geb_list.size(): - var teil: Dictionary = geb_list[i] - var g_child := content_tree.create_item(geb_item) - g_child.set_text(0, "%s #%d" % [BuildOps.kind_label(str(teil.get("id", "?"))), i + 1]) - g_child.set_meta("gebaeude_index", i) + +func _mark_tree_selection_multi(indices: Array) -> void: + tree_mod.mark_tree_selection_multi(indices) + + +## Rechte Panel-Sektion synchronisieren (wird von interaction.gd gerufen). +func _sync_selection_panel() -> void: + selection_mod.sync_selection() + + +## Hilfs-Meldung zur frischen Auswahl (wird von interaction.gd gerufen). +func _selection_status(idx: int) -> void: + selection_mod.selection_status(idx) func _refresh_stats() -> void: @@ -3301,6 +2046,63 @@ func _fmt_int(value: int) -> String: return s + out +# ------------------------------------------------------- U1 Dock-Zustand ---- + +## Splitter-Lagen (Assets-Höhe) aus der Config lesen — NACH _build_ui. +func _load_dock_state() -> void: + var cfg := ConfigFile.new() + if cfg.load(UIScale.cfg_path()) == OK: + var l: Variant = cfg.get_value(UIScale.WIN_SECTION, "dock_links", null) + if l is float or l is int: + _dock_offsets["links"] = float(l) + var r: Variant = cfg.get_value(UIScale.WIN_SECTION, "dock_rechts", null) + if r is float or r is int: + _dock_offsets["rechts"] = float(r) + var u: Variant = cfg.get_value(UIScale.WIN_SECTION, "dock_unten", null) + if u is float or u is int: + _dock_offsets["unten"] = float(u) + _apply_dock_state() + + +## Gemerkte Lagen anwenden — deferred, weil das Layout erst nach dem ersten +## Layout-Durchlauf vermessene Größen hat. +func _apply_dock_state() -> void: + if DisplayServer.get_name() == "headless": + return + _apply_dock_state_deferred.call_deferred() + + +func _apply_dock_state_deferred() -> void: + if _dock_offsets.is_empty(): + return + var main_split := _find_split("MainSplit") + if main_split != null and _dock_offsets.has("links"): + main_split.split_offset = UIScale.f(float(_dock_offsets["links"])) + var bottom := _find_split("BottomSplit") + if bottom != null and _dock_offsets.has("unten"): + bottom.split_offset = -UIScale.f(float(_dock_offsets["unten"])) + + +func _find_split(sname: String) -> SplitContainer: + var found := find_children(sname, "SplitContainer", true, false) + return found[0] if not found.is_empty() else null + + +## Splitter-Lagen merken (beim Schließen, neben der Fensterlage). +func _save_dock_state() -> void: + if DisplayServer.get_name() == "headless": + return + var main_split := _find_split("MainSplit") + var bottom := _find_split("BottomSplit") + var cfg := ConfigFile.new() + cfg.load(UIScale.cfg_path()) + if main_split != null: + cfg.set_value(UIScale.WIN_SECTION, "dock_links", main_split.split_offset) + if bottom != null: + cfg.set_value(UIScale.WIN_SECTION, "dock_unten", float(-bottom.split_offset)) + cfg.save(UIScale.cfg_path()) + + # ---------------------------------------------------- Screenshot-Demo ------- ## Desktop-Smoke-Hilfe: `godot ... -- --ghost-demo [x z objekt-id]` parkt die @@ -3412,9 +2214,9 @@ func _start_terrain_demo(args: PackedStringArray) -> void: DirAccess.make_dir_recursive_absolute(out_dir) _set_tool_mode("terrain") - terrain_radius_slider.value = 45.0 - terrain_strength_slider.value = 10.0 - terrain_mode_raise.button_pressed = true + terrain_mod.terrain_radius_slider.value = 45.0 + terrain_mod.terrain_strength_slider.value = 10.0 + terrain_mod.terrain_mode_raise.button_pressed = true # 1) Hügel heben — Pinsel-Ring bleibt als visueller Beweis im Bild. terrain_tool.call("demo_stroke", -80.0, -60.0, TerrainField.Mode.RAISE, 1.4) @@ -3424,7 +2226,7 @@ func _start_terrain_demo(args: PackedStringArray) -> void: await _demo_shot(out_dir + "/terrain_raise.png") # 2) Senke senken (roter Ring). - terrain_mode_lower.button_pressed = true + terrain_mod.terrain_mode_lower.button_pressed = true terrain_tool.call("demo_stroke", 90.0, 50.0, TerrainField.Mode.LOWER, 1.2) terrain_tool.call("demo_park_brush", 90.0, 50.0) _demo_camera(Vector3(90.0, 2.0, 50.0), 150.0, 32.0, -50.0) @@ -3432,7 +2234,7 @@ func _start_terrain_demo(args: PackedStringArray) -> void: await _demo_shot(out_dir + "/terrain_lower.png") # 3) Hügelkuppe glätten (blauer Ring). - terrain_mode_smooth.button_pressed = true + terrain_mod.terrain_mode_smooth.button_pressed = true terrain_tool.call("demo_stroke", -70.0, -50.0, TerrainField.Mode.SMOOTH, 0.9) terrain_tool.call("demo_park_brush", -70.0, -50.0) _demo_camera(Vector3(-70.0, 10.0, -50.0), 130.0, 35.0, 20.0) @@ -3440,8 +2242,8 @@ func _start_terrain_demo(args: PackedStringArray) -> void: await _demo_shot(out_dir + "/terrain_smooth.png") # 4) Undo-Roundtrip: kleiner Strich → Undo → Boden wieder flach. - terrain_mode_raise.button_pressed = true - terrain_radius_slider.value = 30.0 + terrain_mod.terrain_mode_raise.button_pressed = true + terrain_mod.terrain_radius_slider.value = 30.0 terrain_tool.call("demo_stroke", 0.0, 150.0, TerrainField.Mode.RAISE, 0.9) terrain_tool.call("demo_park_brush", 0.0, 150.0) _demo_camera(Vector3(0.0, 8.0, 150.0), 120.0, 35.0, 0.0) @@ -3541,7 +2343,7 @@ func _start_spawns_demo(args: PackedStringArray) -> void: # 1) Übersicht: Default-Zonen beider Teams (Kreise) + rote Ghost-Vorschau. _set_tool_mode("spawns") - spawn_team_red.button_pressed = true + check_mod.spawn_team_red.button_pressed = true spawn_tool.call("demo_park_ghost", 260.0, 120.0, 1, 12.0) _demo_camera(Vector3(160.0, 140.0, 0.0), 700.0, 48.0, -90.0) _set_status("Demo 1/4: Spawn-Zonen — blaue Kreise = Team Blau, rote = Team Rot, halbtransparent folgt der Ghost der Maus als Vorschau.") diff --git a/src/ui/panels/assets_panel.gd b/src/ui/panels/assets_panel.gd new file mode 100644 index 0000000..359e040 --- /dev/null +++ b/src/ui/panels/assets_panel.gd @@ -0,0 +1,278 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Objekt-Bibliothek / Assets-Bereich (aus main.gd ausgezogen, Karte t_eaccbfed +## U1 — Logik unverändert, Karten t_3a998ae4/t_59fc0f21): Suche, Klapp-Gruppen, +## Kacheln. Der Bereich wohnt jetzt UNTEN unter der 3D-Ansicht (Unity- +## Projekt-Fenster-Platz); Klick verhält sich noch wie vorher (U5 ändert das). + +const LIBRARY_CFG_SECTION := "library" # Abschnitt in user://scopedvr_editor.cfg + +var library_box: VBoxContainer +var library_buttons: Dictionary = {} # id → Button +## 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 + + +static func spec() -> Dictionary: + return {"titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10} + + +func panel_title() -> String: + return "Objekt-Bibliothek" + + +## Bau (aus der alten main.gd/_build_left_panel übernommen, Logik identisch): +## Suche → „Alle auf/zu“ → Kachel-Liste (library_box) → _fill_library(). +## Der Bereich wohnt jetzt UNTEN (Registry-Gruppe "unten"); Klick verhält sich +## noch wie vorher (U5 ändert das Verhalten). +func build() -> void: + add_child(_header_label("Objekt-Bibliothek")) + add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis ESC (Rechte Maustaste = Flug).")) + + # Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit + # Treffern klappen auf, leere Gruppen verschwinden. + 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) + + # „Alle auf" / „Alle zu" (t_59fc0f21): Klapp-Zustand aller Gruppen auf einmal. + var group_btns := HBoxContainer.new() + group_btns.add_theme_constant_override("separation", UIScale.px(6)) + 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) + + 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) + _fill_library() + + +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). + 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.f(18)) + 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(6)) + grid.add_theme_constant_override("v_separation", UIScale.px(6)) + 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()}) + _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: + (row["header"] as Button).button_pressed = true + + +func _on_library_collapse_all() -> void: + for row in _library_group_rows: + (row["header"] as Button).button_pressed = false + + +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): + out[str(key)] = bool(cfg.get_value(LIBRARY_CFG_SECTION, 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") + + +## Live-Suche (t_59fc0f21): leer = alles normal sichtbar (Klapp-Zustand wie +## gemerkt); Text = nur Treffer (Name de/en, ID — case-insensitive), Gruppen +## mit Treffern bleiben offen, leere verschwinden. +func _apply_library_filter(query: String) -> void: + var q := query.strip_edges().to_lower() + var searching := not q.is_empty() + for row in _library_group_rows: + var ids: Array = row["ids"] + var any := false + for oid in ids: + var btn := library_buttons.get(str(oid)) as Button + if btn == null: + continue + var vis := true + if searching: + vis = _library_oid_matches(str(oid), q) + btn.visible = vis + any = any or vis + var header := row["header"] as Button + var box := row["box"] as VBoxContainer + header.visible = any + if searching: + # Treffer-Gruppen bleiben auf — auch wenn sie zugeklappt waren + # (Pfeil zeigt dann offen, kosmetisch konsistent). + box.visible = any + if any: + header.text = _library_header_text(true, str(row["label"]), int(row["count"])) + else: + var is_open := bool(_library_group_open.get(str(row["key"]), false)) + box.visible = is_open + header.text = _library_header_text(is_open, str(row["label"]), int(row["count"])) + + +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 + + +func _add_library_button(oid: String, parent: Container) -> void: + 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 btn := Button.new() + btn.toggle_mode = true + btn.button_pressed = false + btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + btn.custom_minimum_size = UIScale.v2(0, 126) + btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid] + btn.pressed.connect(_on_library_pressed.bind(oid)) + 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): 96-px-Vorschaubild oben, Name klein darunter. + # Kinder ignorieren die Maus, damit die Klicks am Button ankommen. + var tile := VBoxContainer.new() + tile.mouse_filter = Control.MOUSE_FILTER_IGNORE + tile.set_anchors_preset(Control.PRESET_FULL_RECT) + tile.add_theme_constant_override("separation", UIScale.px(2)) + btn.add_child(tile) + 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 lbl := Label.new() + lbl.text = name_txt + lbl.add_theme_font_size_override("font_size", UIScale.f(11)) + 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) + parent.add_child(btn) + library_buttons[oid] = btn + + +func _on_library_pressed(oid: String) -> void: + if str(main.get("tool_mode")) != "objects": + main.call("_set_tool_mode", "objects") # Bibliothek-Klick wechselt automatisch zurück + if main.interaction.placing and main.interaction.place_oid == oid: + # Nochmal geklickt → Modus beenden (An/Aus ohne Umwege). + main.interaction.cancel_placement() + main.call("_set_status", "Platzieren abgebrochen.") + return + for other_oid in library_buttons: + (library_buttons[other_oid] as Button).button_pressed = false + library_buttons[oid].button_pressed = true + main.interaction.start_placement(oid) + + +## Der Bibliothek-Button des aktiven Platzier-Objekts leuchtet auf. +func highlight_library(oid: String) -> void: + for other_oid in library_buttons: + var btn := library_buttons[other_oid] as Button + btn.button_pressed = other_oid == oid + btn.modulate = Color(1, 1, 1) if oid == other_oid else Color(1, 1, 1, 0.85) diff --git a/src/ui/panels/assets_panel.gd.uid b/src/ui/panels/assets_panel.gd.uid new file mode 100644 index 0000000..7153a42 --- /dev/null +++ b/src/ui/panels/assets_panel.gd.uid @@ -0,0 +1 @@ +uid://dxoh6gjklwjna diff --git a/src/ui/panels/check_panel.gd b/src/ui/panels/check_panel.gd new file mode 100644 index 0000000..072bf57 --- /dev/null +++ b/src/ui/panels/check_panel.gd @@ -0,0 +1,146 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Spawn-Check + Spawn-Zonen-Werkzeug-Panel (aus main.gd ausgezogen, Karte +## t_eaccbfed U1 — Logik unverändert, Karte t_c4162c5d): Live-Validierung +## (Loader-Regeln, IMMER sichtbar) + großer Werkzeug-Button + Team/Radius/ +## Löschen. Werkzeug-Ein/Aus bleibt im Koordinator (main.gd). + +var spawn_edit_btn: Button +var spawn_panel: VBoxContainer +var spawn_team_blue: Button +var spawn_team_red: Button +var spawn_radius_slider: HSlider +var spawn_radius_label: Label +var spawn_status: Label # Live-Validierung (IMMER sichtbar, Loader-Regeln) +var spawn_sel_label: Label # Welche Zone ist an? +var spawn_delete_btn: Button + + +static func spec() -> Dictionary: + return {"titel": "Prüfen", "gruppe": "rechts", "order": 50} + + +func panel_title() -> String: + return "Prüfen" + + +## Bau (aus der alten main.gd/_build_right_panel übernommen, Logik identisch): +## Live-Status IMMER sichtbar + Werkzeug-Button + Bedien-Panel (Team/Radius/ +## Auswahl/Löschen). Ein/Aus des Werkzeugs bleibt beim Koordinator (main.gd). +func build() -> void: + # --- Spawn-Zonen (Karte t_c4162c5d): Live-Status IMMER sichtbar --- + spawn_status = Label.new() + spawn_status.text = "" + spawn_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + spawn_status.custom_minimum_size = UIScale.v2(0, 44) + add_child(spawn_status) + add_child(_muted_label("Der Check sagt VOR dem Export, was der Server-Loader verlangen wird (mindestens 4 Zonen pro Team, bekannte Objekt-IDs).")) + + # --- Spawn-Zonen-Werkzeug: großer Button + Bedien-Panel --- + spawn_edit_btn = Button.new() + spawn_edit_btn.text = "🚩 Spawn-Zonen malen" + spawn_edit_btn.custom_minimum_size = UIScale.v2(0, 48) + spawn_edit_btn.toggle_mode = true + spawn_edit_btn.tooltip_text = "Startzonen für Team Blau und Rot auf den Boden malen (TDM)" + spawn_edit_btn.pressed.connect(_on_spawn_btn_pressed) + add_child(spawn_edit_btn) + + spawn_panel = VBoxContainer.new() + spawn_panel.add_theme_constant_override("separation", UIScale.px(6)) + spawn_panel.visible = false + add_child(spawn_panel) + + spawn_panel.add_child(_field_label("Team:")) + var team_row := HBoxContainer.new() + team_row.add_theme_constant_override("separation", UIScale.px(6)) + spawn_panel.add_child(team_row) + var team_group := ButtonGroup.new() + spawn_team_blue = _spawn_team_button("🔵 Team Blau", team_group, 0) + spawn_team_red = _spawn_team_button("🔴 Team Rot", team_group, 1) + spawn_team_blue.button_pressed = true + team_row.add_child(spawn_team_blue) + team_row.add_child(spawn_team_red) + + spawn_radius_label = _field_label("Zonen-Radius: 6 m") + spawn_panel.add_child(spawn_radius_label) + spawn_radius_slider = HSlider.new() + spawn_radius_slider.min_value = SpawnOps.MIN_ZONE_RADIUS + spawn_radius_slider.max_value = SpawnOps.MAX_ZONE_RADIUS + spawn_radius_slider.step = 1.0 + spawn_radius_slider.value = SpawnOps.DEFAULT_ZONE_RADIUS + spawn_radius_slider.custom_minimum_size = UIScale.v2(0, 32) + spawn_radius_slider.value_changed.connect(_on_spawn_radius_changed) + spawn_panel.add_child(spawn_radius_slider) + + spawn_sel_label = Label.new() + spawn_sel_label.text = "Keine Zone ausgewählt." + spawn_sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + spawn_panel.add_child(spawn_sel_label) + spawn_delete_btn = Button.new() + spawn_delete_btn.text = "Zone löschen (Entf)" + spawn_delete_btn.custom_minimum_size = UIScale.v2(0, 40) + spawn_delete_btn.tooltip_text = "Die ausgewählte Spawn-Zone löschen (Strg+Z holt sie zurück)" + spawn_delete_btn.pressed.connect(_on_spawn_delete_pressed) + spawn_panel.add_child(spawn_delete_btn) + + spawn_panel.add_child(_muted_label( + "Klick setzt eine Zone, Klick+Ziehen verschiebt sie. Jede Zone ist " + + "EIN Rückgängig-Schritt (Strg+Z). ESC beendet das Spawn-Werkzeug.")) + + +func _spawn_team_button(text: String, group: ButtonGroup, team: int) -> Button: + var btn := Button.new() + btn.text = text + btn.toggle_mode = true + btn.button_group = group + btn.custom_minimum_size = UIScale.v2(0, 40) + btn.pressed.connect(_on_spawn_team_changed.bind(team)) + return btn + + +func _on_spawn_btn_pressed() -> void: + if spawn_edit_btn.button_pressed: + main.call("_set_tool_mode", "spawns") + else: + main.call("_set_tool_mode", "objects") + + +func _selected_spawn_team() -> int: + return 1 if spawn_team_red.button_pressed else 0 + + +func _on_spawn_team_changed(team: int) -> void: + if main.spawn_tool != null and str(main.get("tool_mode")) == "spawns": + main.spawn_tool.call("set_team", team) + + +func _on_spawn_radius_changed(value: float) -> void: + spawn_radius_label.text = "Zonen-Radius: %d m" % int(value) + if main.spawn_tool != null and str(main.get("tool_mode")) == "spawns": + main.spawn_tool.call("set_radius", value) + + +func _on_spawn_delete_pressed() -> void: + if str(main.get("tool_mode")) == "spawns": + main.spawn_tool.call("delete_selected") + + +## Panel-Zeile „Welche Zone ist angewählt?“ — WIRD vom Spawn-Tool gerufen. +func sync_spawn_selection() -> void: + if main.spawn_tool == null: + return + var team: int = main.spawn_tool.get("selected_team") + var pid: String = str(main.spawn_tool.get("selected_id")) + if team < 0 or pid == "": + spawn_sel_label.text = "Keine Zone ausgewählt.\nKlicke im Viewport auf einen Kreis." + spawn_delete_btn.disabled = true + return + spawn_sel_label.text = "Ausgewählt: %s (%s)\nVerschieben: klicken+ziehen · Radius: Regler oben" % [ + pid, SpawnOps.team_label(team)] + spawn_delete_btn.disabled = false + + +## LIVE-Validierung (Loader-Regeln) — immer sichtbar, nach JEDEM Struktur-Wechsel. +func refresh_spawn_status() -> void: + if main.doc.is_empty() or spawn_status == null: + return + spawn_status.text = SpawnOps.live_status(main.doc, main.manifest) diff --git a/src/ui/panels/check_panel.gd.uid b/src/ui/panels/check_panel.gd.uid new file mode 100644 index 0000000..6e2389a --- /dev/null +++ b/src/ui/panels/check_panel.gd.uid @@ -0,0 +1 @@ +uid://b84fxjtuhejui diff --git a/src/ui/panels/editor_panel.gd b/src/ui/panels/editor_panel.gd new file mode 100644 index 0000000..345f400 --- /dev/null +++ b/src/ui/panels/editor_panel.gd @@ -0,0 +1,53 @@ +extends VBoxContainer +class_name EditorPanel + +## Basis-Klasse aller Registry-Panels (Karte t_eaccbfed). Jedes Modul baut +## seinen eigenen Inhalt in setup(main) und wird von main.gd NUR über die +## Registry (src/ui/panels/registry.gd) in den Baum gehängt — main.gd kennt +## keine Panel-Interna mehr. +## +## Datenfluss (bewusst 1:1 wie vorher): Module greifen über `main` auf das +## Dokument/History/Werkzeuge zu und rufen die Koordinator-Helfer über +## main.call(...) — der Aufruf-Vertrag zu den Tools (interaction.gd etc.) +## bleibt unverändert, die Methoden wohnen jetzt nur in den Modulen. + +var main # src/ui/main.gd (script-erweitert → untypisiert) + + +func setup(p_main) -> void: + main = p_main + add_theme_constant_override("separation", 8) + + +## Titel der Sektions-Überschrift (Registry-Titel als Default). +func panel_title() -> String: + return "Panel" + + +# --- Gemeinsame Bau-Helfer (aus main.gd übernommen, Logik identisch) -------- + +func _header_label(text: String) -> Label: + var label := Label.new() + label.text = text + label.add_theme_font_size_override("font_size", UIScale.f(19)) + label.add_theme_color_override("font_color", Color(0.55, 0.8, 1.0)) + return label + + +func _field_label(text: String) -> Label: + var label := Label.new() + label.text = text + label.modulate = Color(1, 1, 1, 0.7) + return label + + +func _muted_label(text: String) -> Label: + var label := Label.new() + label.text = text + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.modulate = Color(1, 1, 1, 0.55) + return label + + +func _separator() -> HSeparator: + return HSeparator.new() diff --git a/src/ui/panels/editor_panel.gd.uid b/src/ui/panels/editor_panel.gd.uid new file mode 100644 index 0000000..902fabd --- /dev/null +++ b/src/ui/panels/editor_panel.gd.uid @@ -0,0 +1 @@ +uid://cu0oq4cx0oy60 diff --git a/src/ui/panels/map_panel.gd b/src/ui/panels/map_panel.gd new file mode 100644 index 0000000..4c4b08e --- /dev/null +++ b/src/ui/panels/map_panel.gd @@ -0,0 +1,127 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Map-Eigenschaften (aus main.gd ausgezogen, Karte t_eaccbfed U1 — Logik +## unverändert): Name/Autor/Beschreibung/Version/Wetter + Terrain-Info. +## Eingaben schreiben LIVE ins doc (Live-Bindung wie vorher) und markieren +## dirty — die Signale wurden vorher in _build_right_panel verdrahtet, sie +## hängen jetzt hier. + +var prop_name: LineEdit +var prop_author: LineEdit +var prop_desc: LineEdit +var prop_version: SpinBox +var prop_weather: OptionButton +var terrain_info: Label + +var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder + + +static func spec() -> Dictionary: + return {"titel": "Map", "gruppe": "rechts", "order": 30} + + +func panel_title() -> String: + return "Map-Eigenschaften" + + +## Bau (aus der alten main.gd/_build_right_panel übernommen, Logik identisch): +## Name/Autor/Beschreibung/Version/Wetter + Terrain-Info. Die Signale hängen +## jetzt hier (statt in main.gd) — der doc-Datenfluss ist derselbe. +func build() -> void: + add_child(_field_label("Name")) + prop_name = LineEdit.new() + prop_name.max_length = 64 + add_child(prop_name) + + add_child(_field_label("Autor")) + prop_author = LineEdit.new() + add_child(prop_author) + + add_child(_field_label("Beschreibung")) + prop_desc = LineEdit.new() + add_child(prop_desc) + + add_child(_field_label("Version")) + prop_version = SpinBox.new() + prop_version.min_value = 1 + prop_version.max_value = 99999 + prop_version.step = 1 + add_child(prop_version) + + add_child(_field_label("Wetter")) + prop_weather = OptionButton.new() + prop_weather.add_item("Mittag (sonnig, klar)") + prop_weather.add_item("Abend (warmes Licht)") + prop_weather.add_item("Regen (grau, kühl)") + prop_weather.add_item("Nacht (dunkel)") + add_child(prop_weather) + + add_child(_separator()) + add_child(_header_label("Terrain")) + terrain_info = Label.new() + terrain_info.text = "" + terrain_info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + add_child(terrain_info) + + # Signale (Änderungen → Dokument + Ansicht) — wie vorher in main.gd. + prop_name.text_changed.connect(_on_prop_changed.bind("name")) + prop_author.text_changed.connect(_on_prop_changed.bind("author")) + prop_desc.text_changed.connect(_on_prop_changed.bind("description")) + prop_version.value_changed.connect(_on_version_changed) + prop_weather.item_selected.connect(_on_weather_changed) + + +func _on_prop_changed(text: String, field: String) -> void: + if _loading_ui_values or main.doc.is_empty(): + return + var info: Dictionary = main.doc["info"] + match field: + "name": + info["name"] = text + "author": + info["author"] = text + "description": + info["description"] = text + main.dirty = true + main.call("_refresh_file_label") + + +func _on_version_changed(value: float) -> void: + if _loading_ui_values or main.doc.is_empty(): + return + main.doc["info"]["version"] = int(value) + main.dirty = true + main.call("_refresh_file_label") + + +func _on_weather_changed(index: int) -> void: + if _loading_ui_values or main.doc.is_empty(): + return + var weathers := ["mittag", "abend", "regen", "nacht"] + main.doc["weather"] = weathers[index] + main.dirty = true + main.call("_refresh_file_label") + # Licht/Stimmung im Viewport sofort anpassen + main.view_builder.call("rebuild", main.doc, main.manifest) + main.call("_sync_grid_to_builder") + main.call("_set_status", "Wetter geändert auf „%s“." % weathers[index]) + + +## Felder aus dem doc auffüllen (aus main.gd _refresh_all-Anfang ausgezogen). +func refresh_values() -> void: + _loading_ui_values = true + var info: Dictionary = main.doc.get("info", {}) + prop_name.text = str(info.get("name", "")) + prop_author.text = str(info.get("author", "")) + prop_desc.text = str(info.get("description", "")) + prop_version.value = float(info.get("version", 1)) + var weathers := ["mittag", "abend", "regen", "nacht"] + var weather := str(main.doc.get("weather", "mittag")) + prop_weather.select(maxi(0, weathers.find(weather))) + + var terrain: Dictionary = main.doc.get("terrain", {}) + terrain_info.text = "Größe: %.0f × %.0f m\nAuflösung: %d × %d Höhenpunkte\nHöhenbereich: %.0f bis %.0f m" % [ + float(terrain.get("size_x_m", 0.0)), float(terrain.get("size_z_m", 0.0)), + int(terrain.get("resolution", 0)), + int(terrain.get("resolution", 0)), + float(terrain.get("min_height_m", 0.0)), float(terrain.get("max_height_m", 0.0))] + _loading_ui_values = false diff --git a/src/ui/panels/map_panel.gd.uid b/src/ui/panels/map_panel.gd.uid new file mode 100644 index 0000000..41e082d --- /dev/null +++ b/src/ui/panels/map_panel.gd.uid @@ -0,0 +1 @@ +uid://dihu04ppa406d diff --git a/src/ui/panels/object_tools_panel.gd b/src/ui/panels/object_tools_panel.gd new file mode 100644 index 0000000..bf9fc33 --- /dev/null +++ b/src/ui/panels/object_tools_panel.gd @@ -0,0 +1,34 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Objekt-Werkzeuge (aus main.gd ausgezogen, Karte t_eaccbfed U1 — Logik +## unverändert): „Alles auf den Boden setzen" + Raster-Snap-Schalter. Vor U1 +## wohnten diese beiden Widgets in der linken Spalte unter der Bibliothek; +## sie sind als eigener Registry-Bereich (Gruppe "unten") erhalten geblieben, +## damit nichts verschwindet. + +var snap_btn: Button +var grid_check: CheckButton + + +static func spec() -> Dictionary: + return {"titel": "Objekt-Werkzeuge", "gruppe": "unten", "order": 20} + + +func panel_title() -> String: + return "Objekt-Werkzeuge" + + +func build() -> void: + snap_btn = Button.new() + snap_btn.text = "⚡ Alles auf den Boden setzen" + snap_btn.custom_minimum_size = UIScale.v2(0, 44) + snap_btn.tooltip_text = "Setzt ALLE Objekte auf die Terrain-Höhe — z. B. nach dem Terrain-Formen, wenn etwas in der Luft hängt" + snap_btn.pressed.connect(func() -> void: main.call("_on_snap_to_ground")) + add_child(snap_btn) + + grid_check = CheckButton.new() + grid_check.text = "Raster-Snap (1 m)" + grid_check.custom_minimum_size = UIScale.v2(0, 40) + grid_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein" + grid_check.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_snap_changed", pressed)) + add_child(grid_check) + add_child(_muted_label("Raster-Snap rastet Platzieren und Ziehen auf ganze Meter.")) \ No newline at end of file diff --git a/src/ui/panels/object_tools_panel.gd.uid b/src/ui/panels/object_tools_panel.gd.uid new file mode 100644 index 0000000..dc9120e --- /dev/null +++ b/src/ui/panels/object_tools_panel.gd.uid @@ -0,0 +1 @@ +uid://cs4xao3sf5y7n diff --git a/src/ui/panels/registry.gd b/src/ui/panels/registry.gd new file mode 100644 index 0000000..c2511d8 --- /dev/null +++ b/src/ui/panels/registry.gd @@ -0,0 +1,87 @@ +class_name PanelRegistry +extends RefCounted + +## Zentrale, DATENGETRIEBENE Anmeldung aller Editor-Bereiche (Karte t_eaccbfed, +## Operator-Vorgabe 20.10.). main.gd baut das Layout AUSSCHLIESSLICH aus dieser +## Registry — keine Panel-Erzeugung mehr fest verdrahtet. +## +## Warum: Die Umbau-Karten U2 (Szenen-Baum), U3 (Gruppen), U4 (Assets), +## U5 (Platzieren nur einmal), U6 (dezente Auswahl) und U7 (Feinschliff) ändern +## danach NUR ihr eigenes Modul und melden sich HIER an (einzeiliger Eintrag) +## — ohne main.gd anzufassen. Dadurch kollidieren parallele Karten nicht mehr +## in derselben Datei (gleiche Falle wie früher bei match_hud.gd). +## +## Struktur je Eintrag: titel (Tab-/Überschrift), gruppe ("rechts" = rechtes +## TabControl, "links" = linke Spalte, "unten" = Assets-Bereich, "toolbar" / +## "status" = Leisten-Aktionen), order (Sortier-Schlüssel), skript (preload-Pfad +## des Panel-Moduls). Ein Bereich erscheint GENAU EINMAL im Baum (test_ui_layout +## prüft das gegen Doppel-Namen). + +const TITLE_TOOLBAR := "Werkzeuge" +const TITLE_STATUS := "Statuszeile" + +## Die Bereiche in fester Reihenfolge. skript = preload-Pfad; jeder +## Panel-Eintrag muss in seinem Skript `static func spec()` haben, das +## mindestens {"titel", "gruppe", "order"} liefert (die Werte hier sind die +## Authorität — spec() wird von test_ui_layout GEGEN diese Tabelle geprüft). +const PANELS: Array = [ + # --- rechts: Tab-Gruppe (Unity-Inspector-Spalte) --- + {"id": "inspektor", "titel": "Inspektor", "gruppe": "rechts", "order": 10, + "skript": "res://src/ui/inspector_panel.gd"}, + {"id": "auswahl", "titel": "Auswahl", "gruppe": "rechts", "order": 20, + "skript": "res://src/ui/panels/selection_panel.gd"}, + {"id": "map", "titel": "Map", "gruppe": "rechts", "order": 30, + "skript": "res://src/ui/panels/map_panel.gd"}, + {"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40, + "skript": "res://src/ui/panels/terrain_panel.gd"}, + {"id": "check", "titel": "Prüfen", "gruppe": "rechts", "order": 50, + "skript": "res://src/ui/panels/check_panel.gd"}, + # --- links: Vorbereitet für U2 (Szenen-Baum); Bibliothek ist UNTEN --- + {"id": "szene", "titel": "Map-Inhalt", "gruppe": "links", "order": 10, + "skript": "res://src/ui/panels/scene_tree_panel.gd"}, + # --- unten: Objekt-Bibliothek / Assets (Unity-Projekt-Fenster-Platz) --- + {"id": "assets", "titel": "Objekt-Bibliothek", "gruppe": "unten", "order": 10, + "skript": "res://src/ui/panels/assets_panel.gd"}, + # Objekt-Werkzeuge (Boden-Setzen/Raster-Snap, vor U1 links) — erhalten + # geblieben als eigener Tab unten, damit nichts verschwindet. + {"id": "werkzeuge", "titel": "Objekt-Werkzeuge", "gruppe": "unten", "order": 20, + "skript": "res://src/ui/panels/object_tools_panel.gd"}, +] + +## Leisten-Aktionen (Toolbar-Buttons/Menüs — KEINE Panels, melden sich ditto +## datengetrieben an, damit T1s Lauf-Modus und künftige Aktionen main.gd +## nicht mehr anfassen). action = Methodenname an main (Callable wird gebunden). +const ACTIONS: Array = [ + {"id": "karte_pruefen", "titel": "✅ Karte prüfen", "gruppe": "toolbar", + "order": 10, "action": "_on_check_map"}, + {"id": "karte_testen", "titel": "▶ Karte testen", "gruppe": "toolbar", + "order": 20, "action": "_on_test_run"}, +] + + +## Panel-Einträge einer Gruppe, nach order sortiert (Reihenfolge = Tab-Reihenfolge). +static func entries(gruppe: String) -> Array: + var out: Array = [] + for e in PANELS: + if str(e["gruppe"]) == gruppe: + out.append(e) + out.sort_custom(func(a, b) -> bool: return int(a["order"]) < int(b["order"])) + return out + + +## Toolbar-Aktionen, nach order sortiert. +static func toolbar_actions() -> Array: + var out: Array = [] + for e in ACTIONS: + if str(e["gruppe"]) == "toolbar": + out.append(e) + out.sort_custom(func(a, b) -> bool: return int(a["order"]) < int(b["order"])) + return out + + +## Eintrag per ID (für Tests/Probes). +static func entry(id: String) -> Dictionary: + for e in PANELS: + if str(e["id"]) == id: + return e + return {} diff --git a/src/ui/panels/registry.gd.uid b/src/ui/panels/registry.gd.uid new file mode 100644 index 0000000..78557b8 --- /dev/null +++ b/src/ui/panels/registry.gd.uid @@ -0,0 +1 @@ +uid://cmom62lxsqgef diff --git a/src/ui/panels/scene_tree_panel.gd b/src/ui/panels/scene_tree_panel.gd new file mode 100644 index 0000000..f051267 --- /dev/null +++ b/src/ui/panels/scene_tree_panel.gd @@ -0,0 +1,155 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Map-Inhalt-Baum (aus main.gd ausgezogen, Karte t_eaccbfed U1 — Logik +## unverändert). U2 baut hier den hierarchischen Szenen-Baum aus; diese Karte +## zieht nur um: Der Baum lebt jetzt im LINKEN TabContainer (Registry-Gruppe +## "links"), Zustand/Signale wie vorher. + +var content_tree: Tree +var _tree_select_lock := false # verhindert Rückkopplung Auswahl ↔ Baum + + +static func spec() -> Dictionary: + return {"titel": "Map-Inhalt", "gruppe": "links", "order": 10} + + +func build() -> void: + add_child(_header_label("Map-Inhalt")) + + content_tree = Tree.new() + content_tree.size_flags_vertical = Control.SIZE_EXPAND_FILL + content_tree.custom_minimum_size = UIScale.v2(0, 160) + content_tree.allow_reselect = true + content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2) + content_tree.hide_root = true + content_tree.item_selected.connect(_on_tree_selected) + add_child(content_tree) + + +## Der Baum ruft zurück, wenn der Nutzer dort ein Objekt auswählt. +## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl, +## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln +## sich in die Viewport-Auswahl (Baum und Szene sind immer synchron). +func _on_tree_selected() -> void: + if _tree_select_lock: + return + var item := content_tree.get_selected() + if item == null: + return + if item.has_meta("gebaeude_index"): + _on_tree_selected_gebaeude(int(item.get_meta("gebaeude_index"))) + return + var indices: Array = [] + var walk := content_tree.get_next_selected(null) + while walk != null: + if walk.has_meta("obj_index"): + indices.append(int(walk.get_meta("obj_index"))) + walk = content_tree.get_next_selected(walk) + if indices.is_empty(): + return + var objects: Array = main.doc.get("objects", []) + var valid: Array = [] + for idx in indices: + if idx >= 0 and idx < objects.size(): + valid.append(idx) + if valid.is_empty(): + return + if valid.size() == 1: + main.interaction.select_object(valid[0], true) + main.call("_set_status", "%s ausgewählt — ziehen zum Verschieben, R/Mausrad zum Drehen, Entf zum Löschen. Strg+Klick markiert weitere dazu." % main.object_label(str(objects[valid[0]].get("id", "")))) + else: + main.interaction.select_many(valid) + main.call("_set_status", "%d Objekte im Baum markiert — Entf löscht ALLE, Strg+D dupliziert ALLE (jeweils EIN Undo-Schritt)." % valid.size()) + + +## Baum: Bauteil-Klick → Bauen-Modus + Teil anwählen (Karte t_97ea59c5). +func _on_tree_selected_gebaeude(gi: int) -> void: + if str(main.get("tool_mode")) != "bauen": + main.call("_set_tool_mode", "bauen") + main.build_tool.call("select_index", gi) + + +func mark_tree_selection(idx: int) -> void: + mark_tree_selection_multi([idx]) + + +func mark_tree_selection_multi(indices: Array) -> void: + _tree_select_lock = true + var wanted := {} + for i in indices: + wanted[int(i)] = true + _mark_tree_selection_walk(content_tree.get_root(), wanted) + var primary: Variant = main.interaction.get("selected_index") if main.interaction != null else -1 + if primary is int and primary >= 0: + _ensure_tree_visible(content_tree.get_root(), primary) + _tree_select_lock = false + + +func _mark_tree_selection_walk(item: TreeItem, wanted: Dictionary) -> void: + if item == null: + return + if item.has_meta("obj_index"): + var idx := int(item.get_meta("obj_index")) + if wanted.has(idx): + item.select(0) + elif item.is_selected(0): + item.deselect(0) + for child in item.get_children(): + _mark_tree_selection_walk(child, wanted) + + +func _ensure_tree_visible(item: TreeItem, idx: int) -> void: + if item == null: + return + if item.has_meta("obj_index") and int(item.get_meta("obj_index")) == idx: + # Fix (beim Inspektor-Einbau gefunden, Vorbestand t_f6f29d86): Tree + # und TreeItem haben KEIN ensure_visible — die Godot-4-API heißt + # Tree.scroll_to_item. Der alte Ruf warf bei JEDER Auswahl einen + # SCRIPT ERROR und scrollte den Baum nie zur Auswahl. + content_tree.scroll_to_item(item) + return + for child in item.get_children(): + _ensure_tree_visible(child, idx) + + +func refresh_tree() -> void: + content_tree.clear() + var root := content_tree.create_item() + var objects: Array = main.doc.get("objects", []) + + var obj_item := content_tree.create_item(root) + obj_item.set_text(0, "Objekte (%d)" % objects.size()) + for i in objects.size(): + var obj = objects[i] + var item := content_tree.create_item(obj_item) + if obj is Dictionary: + item.set_text(0, "%s #%d" % [main.object_label(str(obj.get("id", "?"))), i + 1]) + item.set_meta("obj_index", i) + else: + item.set_text(0, "ungültig #%d" % (i + 1)) + + var zones: Dictionary = main.doc.get("spawn_zones", {}) + var spawn_item := content_tree.create_item(root) + spawn_item.set_text(0, "Spawn-Zonen") + for team_key in ["0", "1"]: + if not zones.has(team_key): + continue + var zone: Dictionary = zones[team_key] + var team_item := content_tree.create_item(spawn_item) + var team_name := "Team Blau" if team_key == "0" else "Team Rot" + team_item.set_text(0, "%s (%d Punkte)" % [team_name, (zone.get("points", []) as Array).size()]) + for p in (zone.get("points", []) as Array): + if p is Dictionary: + var point_item := content_tree.create_item(team_item) + point_item.set_text(0, "%s (%.0f, %.0f)" % [ + str(p.get("id", "?")), float(p.get("pos", [0, 0, 0])[0]), float(p.get("pos", [0, 0, 0])[2])]) + + # Gebäude (Karte t_97ea59c5): Bauteile im Baum — Klick wählt das Teil an. + var geb_list: Array = BuildOps.pieces(main.doc) + if not geb_list.is_empty(): + var geb_item := content_tree.create_item(root) + geb_item.set_text(0, "Gebäude (%d Bauteile)" % geb_list.size()) + for i in geb_list.size(): + var teil: Dictionary = geb_list[i] + var g_child := content_tree.create_item(geb_item) + g_child.set_text(0, "%s #%d" % [BuildOps.kind_label(str(teil.get("id", "?"))), i + 1]) + g_child.set_meta("gebaeude_index", i) diff --git a/src/ui/panels/scene_tree_panel.gd.uid b/src/ui/panels/scene_tree_panel.gd.uid new file mode 100644 index 0000000..a7e6184 --- /dev/null +++ b/src/ui/panels/scene_tree_panel.gd.uid @@ -0,0 +1 @@ +uid://bpbw3vo36ohv1 diff --git a/src/ui/panels/selection_panel.gd b/src/ui/panels/selection_panel.gd new file mode 100644 index 0000000..7658bfe --- /dev/null +++ b/src/ui/panels/selection_panel.gd @@ -0,0 +1,234 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Auswahl-Panel (U1, Karte t_eaccbfed — aus main.gd ausgezogen, Logik +## UNVERÄNDERT von dort übernommen): Buttons Drehen/Löschen/Duplizieren/ +## Spiegeln/Anheben/Absenken + Gruppen-Buttons + Gizmo-Schrittweite. +## +## Verdrahtung: main.gd ruft sync_selection() an allen Auswahl-Wegen +## (vorher: _sync_selection_panel) und selection_status(idx) bei frischer +## Auswahl (vorher: _selection_status). Alle Objekt-Operationen laufen über +## main.interaction — der Vertrag zu interaction.gd bleibt identisch. + +var sel_label: Label +var sel_rotate_btn: Button +var sel_delete_btn: Button +var sel_dup_btn: Button +var sel_mirror_x_btn: Button +var sel_mirror_z_btn: Button +var sel_raise_btn: Button +var sel_lower_btn: Button +var sel_group_btn: Button +var sel_ungroup_btn: Button +var gizmo_step_box: SpinBox + + +static func spec() -> Dictionary: + return {"titel": "Auswahl", "gruppe": "rechts", "order": 20} + + +func panel_title() -> String: + return "Auswahl" + + +func build() -> void: + # --- Auswahl (Anfassen/Greifen) — 1:1 aus der alten _build_right_panel --- + sel_label = Label.new() + sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an." + sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + add_child(sel_label) + var sel_btns := HBoxContainer.new() + sel_btns.add_theme_constant_override("separation", UIScale.px(8)) + add_child(sel_btns) + sel_rotate_btn = Button.new() + sel_rotate_btn.text = "Drehen (R)" + sel_rotate_btn.custom_minimum_size = UIScale.v2(140, 40) + sel_rotate_btn.tooltip_text = "Ausgewähltes Objekt 15° drehen (auch: Mausrad)" + sel_rotate_btn.pressed.connect(func() -> void: main.interaction.rotate_selected(15.0)) + sel_btns.add_child(sel_rotate_btn) + sel_delete_btn = Button.new() + sel_delete_btn.text = "Löschen (Entf)" + sel_delete_btn.custom_minimum_size = UIScale.v2(140, 40) + sel_delete_btn.tooltip_text = "Ausgewähltes Objekt löschen (Strg+Z holt es zurück)" + sel_delete_btn.pressed.connect(main.interaction.delete_selected) + sel_btns.add_child(sel_delete_btn) + + # Objekt-Ops (Karte t_7567a06f): Duplizieren · Spiegeln · Anheben/Absenken. + var dup_row := HBoxContainer.new() + dup_row.add_theme_constant_override("separation", UIScale.px(8)) + add_child(dup_row) + sel_dup_btn = _sel_op_button("⧉ Duplizieren (Strg+D)", + "Legt eine Kopie daneben — leicht versetzt, direkt angewählt. Strg+D geht auch.", + func() -> void: main.interaction.duplicate_selected()) + dup_row.add_child(sel_dup_btn) + + var mirror_row := HBoxContainer.new() + mirror_row.add_theme_constant_override("separation", UIScale.px(8)) + add_child(mirror_row) + sel_mirror_x_btn = _sel_op_button("⇄ Links↔Rechts", + "Spiegelt das Objekt an seiner Stelle (links↔rechts, X-Achse) — die Blickrichtung wird mitgespiegelt.", + func() -> void: main.interaction.mirror_selected("x")) + mirror_row.add_child(sel_mirror_x_btn) + sel_mirror_z_btn = _sel_op_button("⇅ Vor↔Zurück", + "Spiegelt das Objekt an seiner Stelle (vor↔zurück, Z-Achse) — die Blickrichtung wird mitgespiegelt.", + func() -> void: main.interaction.mirror_selected("z")) + mirror_row.add_child(sel_mirror_z_btn) + + var lift_row := HBoxContainer.new() + lift_row.add_theme_constant_override("separation", UIScale.px(8)) + add_child(lift_row) + sel_raise_btn = _sel_op_button("▲ Anheben (Bild↑)", + "Objekt 0,25 m anheben — es schwebt dann, auch wenn der Boden darunter anders ist. Bild↑ geht auch.", + func() -> void: main.interaction.lift_selected(ObjectOps.LIFT_STEP_M)) + lift_row.add_child(sel_raise_btn) + sel_lower_btn = _sel_op_button("▼ Absenken (Bild↓)", + "Objekt 0,25 m absenken — stoppt am Boden (nichts wird in Hügel begraben). Bild↓ geht auch.", + func() -> void: main.interaction.lift_selected(-ObjectOps.LIFT_STEP_M)) + lift_row.add_child(sel_lower_btn) + + # Gruppen-Buttons (Karte t_f6f29d86): Mehrfachauswahl → Gruppe + var group_btns := HBoxContainer.new() + group_btns.add_theme_constant_override("separation", UIScale.px(8)) + add_child(group_btns) + sel_group_btn = Button.new() + sel_group_btn.text = "🔗 Gruppieren" + sel_group_btn.custom_minimum_size = UIScale.v2(150, 40) + sel_group_btn.tooltip_text = "Ausgewählte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam" + sel_group_btn.pressed.connect(func() -> void: main.interaction.group_selection()) + group_btns.add_child(sel_group_btn) + sel_ungroup_btn = Button.new() + sel_ungroup_btn.text = "⛓ Gruppe lösen" + sel_ungroup_btn.custom_minimum_size = UIScale.v2(150, 40) + sel_ungroup_btn.tooltip_text = "Die Gruppe der Auswahl wieder auflösen (Strg+Umschalt+G) — Objekte bleiben, nur die Bindung löst sich" + sel_ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection()) + group_btns.add_child(sel_ungroup_btn) + add_child(_muted_label("Mehrfachauswahl: Umschalt+Klick oder Rahmen aufziehen. Eine Gruppe wandert und dreht gemeinsam — auch nach Speichern/Laden.")) + + # --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen- + # Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die + # Voreinstellungen (fein/normal/grob); dieses Feld setzt sie exakt und + # überlebt UI-Rebuilds, weil der Wert in main.gizmo_step_m liegen bleibt. + add_child(_header_label("Gizmo-Einstellungen")) + var gizmo_row := HBoxContainer.new() + gizmo_row.add_theme_constant_override("separation", UIScale.px(8)) + add_child(gizmo_row) + gizmo_row.add_child(_field_label("Schrittweite")) + gizmo_step_box = SpinBox.new() + gizmo_step_box.min_value = 0.01 + gizmo_step_box.max_value = 10.0 + gizmo_step_box.step = 0.01 + gizmo_step_box.value = main.gizmo_step_m + gizmo_step_box.custom_minimum_size = UIScale.v2(110, 30) + gizmo_step_box.tooltip_text = "Wie weit bewegt sich das Objekt am Achsen-Gizmo pro Schritt (in Metern). Mausrad WÄHREND des Ziehens wechselt zwischen fein (0.1) / normal (0.5) / grob (2.0)." + gizmo_row.add_child(gizmo_step_box) + gizmo_step_box.value_changed.connect(func(v: float) -> void: + main.gizmo_step_m = v + if main.gizmo != null and is_instance_valid(main.gizmo): + main.gizmo.set_step_m(v) + ) + add_child(_separator()) + + +## Ein Auswahl-Button mit Klartext-Tipp (aus main.gd übernommen). +func _sel_op_button(text: String, tip: String, action: Callable) -> Button: + var btn := Button.new() + btn.text = text + btn.custom_minimum_size = UIScale.v2(140, 40) + btn.tooltip_text = tip + btn.pressed.connect(action) + return btn + + +## Objekt-Inspektor + Auswahl-Panel synchron halten (vorher +## main.gd/_sync_selection_panel — Logik unverändert). +func sync_selection() -> void: + # Objekt-Inspektor mit allen Auswahl-Wege synchron halten (Karte t_bb0d11c9). + if main.inspector != null: + main.inspector.call("refresh") + # Gizmo-Schrittweite-Feld mitziehen (Karte t_e0120dc2): Das Mausrad + # WÄHREND eines Zugs kann sie im Gizmo ändern (cycle_step) — das Panel- + # Feld folgt ohne eigene value_changed-Rückkopplung (set_value_no_signal). + if gizmo_step_box != null and is_instance_valid(gizmo_step_box) \ + and not gizmo_step_box.has_focus(): + gizmo_step_box.set_value_no_signal(main.gizmo_step_m) + var doc: Dictionary = main.doc + var objects: Array = doc.get("objects", []) + var sel: Array = main.interaction.selected_indices + var valid: Array = [] + for i in sel: + if i is int and i >= 0 and i < objects.size(): + valid.append(i) + var has_sel := not valid.is_empty() + for btn in [sel_rotate_btn, sel_delete_btn, sel_dup_btn, sel_mirror_x_btn, + sel_mirror_z_btn, sel_raise_btn, sel_lower_btn]: + btn.disabled = not has_sel + var has_group := false + for i in valid: + if GroupOps.group_id(objects[i]) != GroupOps.UNGROUPED: + has_group = true + break + sel_group_btn.disabled = valid.size() < 2 + sel_ungroup_btn.disabled = not has_group + if not has_sel: + sel_label.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an." + return + if valid.size() == 1: + var idx: int = valid[0] + var obj: Dictionary = objects[idx] + var pos: Array = obj.get("pos", [0.0, 0.0, 0.0]) + var lift := ObjectOps.lift_offset(doc, idx) + var lift_text := "am Boden" if absf(lift) < 0.02 else ("%+.2f m über dem Boden" % lift) + var group_line := "" + var gid := GroupOps.group_id(obj) + if gid != GroupOps.UNGROUPED: + group_line = "\nGruppe %d — zieht & dreht GEMEINSAM (Lösen: Button unten)" % (gid + 1) + sel_label.text = "%s (Objekt %d von %d)\nPosition: %.0f, %.0f m · Höhe: %s · Richtung: %.0f°%s\nZiehen: verschieben · R oder Mausrad: drehen (15°)\nBild↑/↓: anheben/absenken · Strg+D: duplizieren\nSpiegeln/Löschen: Buttons oben · Strg+Z: zurück" % [ + main.object_label(str(obj.get("id", ""))), idx + 1, objects.size(), + float(pos[0]), float(pos[2]), lift_text, float(obj.get("rot_y_deg", 0.0)), group_line] + return + # Mehrfachauswahl: Liste aller gewählten Objekte (max. 8 Zeilen, dann „…“). + var names := PackedStringArray() + for i in valid: + if names.size() < 8: + names.append(main.object_label(str((objects[i] as Dictionary).get("id", "?")))) + var more := "" + if valid.size() > 8: + more = "\n… und %d weitere" % (valid.size() - 8) + sel_label.text = "%d Objekte ausgewählt:\n%s%s\nGemeinsam: ziehen (Gruppe wandert zusammen), drehen um die Mitte, Entf löscht alle." % [ + valid.size(), " · ".join(names), more] + + +## Hilfs-Meldung zur frischen Auswahl (Klartext + Budget-Warnung + nächster +## Schritt) — vorher main.gd/_selection_status, Logik unverändert. +func selection_status(idx: int) -> void: + # Objekt-Inspektor füllen (Karte t_bb0d11c9) — dieser Weg läuft bei + # 3D-Klick, Rahmen-Ausziehen und Map-Inhalt-Baum. + if main.inspector != null: + main.inspector.call("refresh") + # Fix (Vorbestand, beim Inspektor-Einbau gefunden): select_object/ + # select_many füllten das alte „Auswahl“-Feld NIE — sel_label blieb auf + # „Nichts ausgewählt“ stehen, während die Statuszeile das Objekt nannte. + # sync_selection ändert bewusst KEINEN Statuszeilen-Text (Stummsync). + sync_selection() + var doc: Dictionary = main.doc + var objects: Array = doc.get("objects", []) + var sel: Array = main.interaction.selected_indices + if (idx < 0 or idx >= objects.size()) and sel.is_empty(): + main._base_tool_status() + return + var used := int(SvmapDocument.poly_usage(doc, main.manifest)) + var budget := int(SvmapIO.MAX_TOTAL_POLY) + var budget_warning := "" + if used > budget * 80 / 100: + budget_warning = " ⚠ Poly-Budget zu %.0f %% voll — bald passt kein Objekt mehr rein." % (100.0 * used / budget) + if sel.size() > 1: + main._set_status("%d Objekte ausgewählt — ziehen bewegt ALLE gemeinsam, R/Mausrad dreht sie um die Mitte, Entf löscht alle, Strg+G gruppiert.%s" % [sel.size(), budget_warning]) + return + if idx < 0 or idx >= objects.size(): + main._base_tool_status() + return + var obj: Dictionary = objects[idx] + var gid := GroupOps.group_id(obj) + var group_hint := "" + if gid != GroupOps.UNGROUPED: + group_hint = " (Gruppe %d — die zieht GEMEINSAM)" % (gid + 1) + main._set_status("%s%s ausgewählt — ziehen zum Verschieben, R oder Mausrad zum Drehen, Bild↑/↓ anheben/absenken, Strg+D duplizieren, Entf zum Löschen.%s" % [ + main.object_label(str(obj.get("id", ""))), group_hint, budget_warning]) \ No newline at end of file diff --git a/src/ui/panels/selection_panel.gd.uid b/src/ui/panels/selection_panel.gd.uid new file mode 100644 index 0000000..80c9c4d --- /dev/null +++ b/src/ui/panels/selection_panel.gd.uid @@ -0,0 +1 @@ +uid://dwp3df5nsfl3s diff --git a/src/ui/panels/status_bar.gd b/src/ui/panels/status_bar.gd new file mode 100644 index 0000000..302882e --- /dev/null +++ b/src/ui/panels/status_bar.gd @@ -0,0 +1,58 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Statusleiste (aus main.gd ausgezogen, Karte t_eaccbfed U1 — Logik +## unverändert): Hilfs-Meldungen + permanente Werkzeug-Anzeige + +## Symmetrie-Anzeige + Objekt-/Terrain-Zähler + FPS-Timer. + +var status_label: Label +var tool_label: Label # permanenter Werkzeugmodus + Flug-Hint +var sym_label: Label +var stats_objects: Label +var stats_terrain: Label + + +static func spec() -> Dictionary: + return {"titel": "Statuszeile", "gruppe": "status", "order": 0} + + +func build() -> void: + add_theme_constant_override("separation", UIScale.px(16)) + status_label = Label.new() + status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + status_label.clip_text = true # langer Text darf das Layout NICHT sprengen + add_child(status_label) + + # Karte 3/3 (t_11b5c25b): PERMANENTE Werkzeug-Anzeige — Sly sieht immer, + # in welchem Werkzeug er ist, plus Hint für den Flugmodus (Umschalt+F). + tool_label = Label.new() + tool_label.modulate = Color(0.55, 0.8, 1.0) + # Karte t_297221b5: Statuszeilen-Labels egalisiert (min-size-Druck bei 1.5×) + tool_label.clip_text = true + tool_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + add_child(tool_label) + + # Symmetrie-Anzeige (Karte t_f5935023, M1): Sly sieht in der Statuszeile + # immer, ob/wie gespiegelt wird (Standard: Punkt). + sym_label = Label.new() + sym_label.modulate = Color(0.55, 0.8, 1.0) + sym_label.clip_text = true + sym_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + add_child(sym_label) + + stats_objects = Label.new() + stats_objects.modulate = Color(1, 1, 1, 0.75) + stats_objects.clip_text = true + stats_objects.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + add_child(stats_objects) + + # Terrain-Anteil + Editor-FPS (Perf-Zähler, Karte t_9aba04f1) + stats_terrain = Label.new() + stats_terrain.modulate = Color(1, 1, 1, 0.75) + stats_terrain.clip_text = true + stats_terrain.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + add_child(stats_terrain) + + var fps_timer := Timer.new() + fps_timer.wait_time = 0.5 + fps_timer.autostart = true + fps_timer.timeout.connect(main.call.bind("_refresh_stats")) + add_child(fps_timer) diff --git a/src/ui/panels/status_bar.gd.uid b/src/ui/panels/status_bar.gd.uid new file mode 100644 index 0000000..afaedfb --- /dev/null +++ b/src/ui/panels/status_bar.gd.uid @@ -0,0 +1 @@ +uid://dddgvtqqr3lau diff --git a/src/ui/panels/terrain_panel.gd b/src/ui/panels/terrain_panel.gd new file mode 100644 index 0000000..7348195 --- /dev/null +++ b/src/ui/panels/terrain_panel.gd @@ -0,0 +1,279 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Terrain-Werkzeug-Panel (aus main.gd ausgezogen, Karte t_eaccbfed U1 — +## Logik unverändert, Karte t_9aba04f1 + t_603ec1a8): großer Modus-Button + +## Pinsel-Modi, Material-Palette, Größe/Stärke-Regler. Werkzeug-Ein/Aus bleibt +## im Koordinator (main.gd _enter/_leave terrain), das Panel hält nur die +## Widgets und die Regler-Verdrahtung. + +var terrain_edit_btn: Button +var terrain_panel: VBoxContainer +var terrain_mode_raise: Button +var terrain_mode_lower: Button +var terrain_mode_smooth: Button +var terrain_mode_paint: Button +var terrain_palette: VBoxContainer +var terrain_mat_buttons: Array = [] # Gras/Sand/Fels (ButtonGroup) +var terrain_size_btn_small: Button +var terrain_size_btn_medium: Button +var terrain_size_btn_large: Button +var terrain_radius_slider: HSlider +var terrain_radius_label: Label +var terrain_strength_slider: HSlider +var terrain_strength_label: Label + +# --- Bauen (Karte t_97ea59c5) — im selben Werkzeug-Tab (aus main.gd ausgezogen) +var build_edit_btn: Button +var build_panel: VBoxContainer +var build_kind_buttons: Dictionary = {} # kind_id → Button + + +static func spec() -> Dictionary: + return {"titel": "Terrain", "gruppe": "rechts", "order": 40} + + +func build() -> void: + terrain_edit_btn = Button.new() + terrain_edit_btn.text = "⛰ Terrain formen" + terrain_edit_btn.custom_minimum_size = UIScale.v2(0, 48) + terrain_edit_btn.toggle_mode = true + terrain_edit_btn.tooltip_text = "Boden heben, senken und glätten — direkt in der Karte malen" + terrain_edit_btn.pressed.connect(_on_terrain_btn_pressed) + add_child(terrain_edit_btn) + + terrain_panel = VBoxContainer.new() + terrain_panel.add_theme_constant_override("separation", UIScale.px(6)) + terrain_panel.visible = false + add_child(terrain_panel) + + terrain_panel.add_child(_field_label("Pinsel-Werkzeug:")) + var mode_row := HBoxContainer.new() + mode_row.add_theme_constant_override("separation", UIScale.px(6)) + terrain_panel.add_child(mode_row) + var mode_group := ButtonGroup.new() + terrain_mode_raise = _terrain_mode_button("Heben ⬆", mode_group) + terrain_mode_lower = _terrain_mode_button("Senken ⬇", mode_group) + terrain_mode_smooth = _terrain_mode_button("Glätten ≈", mode_group) + terrain_mode_paint = _terrain_mode_button("Malen 🖌", mode_group) + terrain_mode_raise.button_pressed = true + terrain_mode_raise.toggled.connect(_on_terrain_mode_changed) + terrain_mode_lower.toggled.connect(_on_terrain_mode_changed) + terrain_mode_smooth.toggled.connect(_on_terrain_mode_changed) + terrain_mode_paint.toggled.connect(_on_terrain_mode_changed) + mode_row.add_child(terrain_mode_raise) + mode_row.add_child(terrain_mode_lower) + mode_row.add_child(terrain_mode_smooth) + mode_row.add_child(terrain_mode_paint) + + # Material-Palette (Karte t_603ec1a8): nur sichtbar im Malen-Modus. + terrain_palette = VBoxContainer.new() + terrain_palette.add_theme_constant_override("separation", UIScale.px(4)) + terrain_palette.visible = false + terrain_panel.add_child(terrain_palette) + terrain_palette.add_child(_field_label("Material:")) + var mat_row := HBoxContainer.new() + mat_row.add_theme_constant_override("separation", UIScale.px(6)) + terrain_palette.add_child(mat_row) + var mat_group := ButtonGroup.new() + var mat_labels := ["🌿 Gras", "🏖 Sand", "🪨 Fels"] + for mat_id in SvmapIO.MATERIAL_IDS.size(): + var mat_btn := Button.new() + mat_btn.text = str(mat_labels[mat_id]) + mat_btn.custom_minimum_size = UIScale.v2(0, 40) + mat_btn.toggle_mode = true + mat_btn.button_group = mat_group + mat_btn.tooltip_text = "Malen: %s" % str(SvmapIO.MATERIAL_IDS[mat_id]) + if mat_id == 0: + mat_btn.button_pressed = true + mat_btn.toggled.connect(_on_terrain_material_changed.bind(mat_id)) + terrain_mat_buttons.append(mat_btn) + mat_row.add_child(mat_btn) + + # Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider. + terrain_panel.add_child(_field_label("Pinselgröße:")) + var size_row := HBoxContainer.new() + size_row.add_theme_constant_override("separation", UIScale.px(6)) + terrain_panel.add_child(size_row) + var size_group := ButtonGroup.new() + terrain_size_btn_small = _terrain_size_button("Klein\n(4 m)", size_group, 4.0) + terrain_size_btn_medium = _terrain_size_button("Mittel\n(12 m)", size_group, 12.0) + terrain_size_btn_large = _terrain_size_button("Groß\n(32 m)", size_group, 32.0) + # Kein Signal: Der Handler greift auf terrain_radius_slider zu, der erst + # unten erzeugt wird — beim Panel-Bau wäre das ein Crash (GUI-Start-Bug + # 15.10.). Der Slider startet unten passend mit 12 m (= Mittel). + terrain_size_btn_medium.set_pressed_no_signal(true) + size_row.add_child(terrain_size_btn_small) + size_row.add_child(terrain_size_btn_medium) + size_row.add_child(terrain_size_btn_large) + + terrain_radius_label = _field_label("Pinsel-Größe: 12 m") + terrain_panel.add_child(terrain_radius_label) + terrain_radius_slider = HSlider.new() + terrain_radius_slider.min_value = 2.0 + terrain_radius_slider.max_value = 64.0 + terrain_radius_slider.step = 1.0 + terrain_radius_slider.value = 12.0 + terrain_radius_slider.custom_minimum_size = UIScale.v2(0, 32) + terrain_radius_slider.value_changed.connect(_on_terrain_radius_changed) + terrain_panel.add_child(terrain_radius_slider) + + terrain_strength_label = _field_label("Stärke: 8 m pro Sekunde") + terrain_panel.add_child(terrain_strength_label) + terrain_strength_slider = HSlider.new() + terrain_strength_slider.min_value = 1.0 + terrain_strength_slider.max_value = 20.0 + terrain_strength_slider.step = 0.5 + terrain_strength_slider.value = 8.0 + terrain_strength_slider.custom_minimum_size = UIScale.v2(0, 32) + terrain_strength_slider.value_changed.connect(_on_terrain_strength_changed) + terrain_panel.add_child(terrain_strength_slider) + + terrain_panel.add_child(_muted_label( + "Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Strg+Linksklick macht das Gegenteil — im Malen-Modus malt Strg+Klick Gras zurück. " + + "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). Rechte Maustaste = Flug, ESC beendet den Terrain-Modus.")) + + # --- Bauen (Karte t_97ea59c5): Modular-Builder-Pieces im App-Skelett --- + # (aus main.gd ausgezogen — liegt mit im Terrain-Tab, Werkzeug-Zusammenhang) + build_edit_btn = Button.new() + build_edit_btn.text = "🏠 Bauen (Häuser)" + build_edit_btn.custom_minimum_size = UIScale.v2(0, 48) + build_edit_btn.toggle_mode = true + build_edit_btn.tooltip_text = "Fundamente, Wände, Dächer, Treppen und Türen bauen — im 2-Meter-Raster, wie Bauklötze" + build_edit_btn.pressed.connect(_on_build_btn_pressed) + add_child(build_edit_btn) + + build_panel = VBoxContainer.new() + build_panel.add_theme_constant_override("separation", UIScale.px(6)) + build_panel.visible = false + add_child(build_panel) + + build_panel.add_child(_field_label("Bauteil:")) + var build_grid := GridContainer.new() + build_grid.columns = 2 + build_grid.add_theme_constant_override("h_separation", UIScale.px(6)) + build_grid.add_theme_constant_override("v_separation", UIScale.px(6)) + build_panel.add_child(build_grid) + var build_group := ButtonGroup.new() + for kind_id in BuildOps.KINDS.keys(): + var kind_btn := Button.new() + kind_btn.text = str(BuildOps.KINDS[kind_id]["label"]) + kind_btn.toggle_mode = true + kind_btn.button_group = build_group + kind_btn.custom_minimum_size = UIScale.v2(148, 44) + kind_btn.clip_text = true + kind_btn.tooltip_text = "%s — %s" % [str(BuildOps.KINDS[kind_id]["label"]), str(BuildOps.KINDS[kind_id].get("info", ""))] + kind_btn.pressed.connect(_on_build_kind_pressed.bind(str(kind_id))) + build_kind_buttons[str(kind_id)] = kind_btn + build_grid.add_child(kind_btn) + (build_kind_buttons["fundament"] as Button).set_pressed_no_signal(true) + + build_panel.add_child(_muted_label( + "Linksklick setzt das Bauteil aufs 2-m-Raster — auch AUF andere Teile " + + "(Wände aufs Fundament stapeln, Dach auf die Wände). Jedes Bauteil ist " + + "EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Bauen-Modus.")) + + +func _on_build_btn_pressed() -> void: + if build_edit_btn.button_pressed: + main.call("_set_tool_mode", "bauen") + else: + main.call("_set_tool_mode", "objects") + + +func _on_build_kind_pressed(kind_id: String) -> void: + main.call("_on_build_kind_pressed", kind_id) + + +func _on_terrain_btn_pressed() -> void: + if terrain_edit_btn.button_pressed: + main.call("_set_tool_mode", "terrain") + else: + main.call("_set_tool_mode", "objects") + + +func _terrain_mode_button(text: String, group: ButtonGroup) -> Button: + var btn := Button.new() + btn.text = text + btn.toggle_mode = true + btn.button_group = group + btn.custom_minimum_size = UIScale.v2(0, 40) + return btn + + +func _selected_terrain_mode() -> int: + if terrain_mode_lower.button_pressed: + return TerrainField.Mode.LOWER + if terrain_mode_smooth.button_pressed: + return TerrainField.Mode.SMOOTH + if terrain_mode_paint.button_pressed: + return TerrainField.Mode.PAINT + return TerrainField.Mode.RAISE + + +## Mode-Buttons (ButtonGroup) → Pinsel-Modus; Palette nur im Malen-Modus. +func _on_terrain_mode_changed(_pressed: bool) -> void: + if terrain_palette != null: + terrain_palette.visible = _selected_terrain_mode() == TerrainField.Mode.PAINT + if main.terrain_tool != null: + main.terrain_tool.call("set_brush_mode", _selected_terrain_mode()) + + +## Material-Palette (Karte t_603ec1a8): gewähltes Material an den Pinsel. +func _on_terrain_material_changed(pressed: bool, mat_id: int) -> void: + if pressed and main.terrain_tool != null: + main.terrain_tool.call("set_paint_material", mat_id) + + +## Pinselgrößen-Presets (Karte t_603ec1a8): setzen den Radius-Slider +## (der Label-Text + Ring-Optik hängen am value_changed-Signal). +func _terrain_size_button(text: String, group: ButtonGroup, radius_m: float) -> Button: + var btn := Button.new() + btn.text = text + btn.toggle_mode = true + btn.button_group = group + btn.custom_minimum_size = UIScale.v2(0, 48) + btn.tooltip_text = "Pinselradius %d m" % int(radius_m) + btn.toggled.connect(_on_terrain_size_preset.bind(radius_m)) + return btn + + +func _on_terrain_size_preset(pressed: bool, radius_m: float) -> void: + # Null-Check als Schutznetz: beim Panel-Bau existiert der Slider noch + # nicht (Programmatic-Pressed darf den Handler nie früh treffen). + if pressed and terrain_radius_slider != null: + terrain_radius_slider.value = radius_m + + +func _on_terrain_radius_changed(value: float) -> void: + terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value) + if main.terrain_tool != null: + main.terrain_tool.call("set_radius", value) + + +func _on_terrain_strength_changed(value: float) -> void: + terrain_strength_label.text = "Stärke: %.1f m pro Sekunde" % value + if main.terrain_tool != null: + main.terrain_tool.call("set_strength", value) + + +## Terrain-Pinsel-Größe per [ / ] (Karte t_55257792): durch die 3 Presets +## Klein/Mittel/Groß zyklisch — der Preset-Button wird mit gesetzt (set_pressed +## feuert toggled → Slider + Label + Pinssel-Ring wandern mit). +## (Aus main.gd ausgezogen, Logik unverändert; Koordinator ruft weiter.) +func terrain_brush_size_step(up: bool) -> void: + if terrain_radius_slider == null: + return + var next := MoveMath.brush_next_preset(float(terrain_radius_slider.value), up) + var btn: Button = null + match next: + 4.0: + btn = terrain_size_btn_small + 12.0: + btn = terrain_size_btn_medium + 32.0: + btn = terrain_size_btn_large + if btn != null: + btn.set_pressed(true) # feuert _on_terrain_size_preset → Slider/Label + else: + terrain_radius_slider.value = next + main.terrain_tool.call("set_radius", next) + main.call("_set_status", "Pinsel-Größe: %d m (mit [ / ] durch Klein/Mittel/Groß zyklisch)." % int(next)) diff --git a/src/ui/panels/terrain_panel.gd.uid b/src/ui/panels/terrain_panel.gd.uid new file mode 100644 index 0000000..fe08e19 --- /dev/null +++ b/src/ui/panels/terrain_panel.gd.uid @@ -0,0 +1 @@ +uid://qvp3thn60awa diff --git a/src/ui/panels/toolbar.gd b/src/ui/panels/toolbar.gd new file mode 100644 index 0000000..602a2f5 --- /dev/null +++ b/src/ui/panels/toolbar.gd @@ -0,0 +1,157 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Toolbar (aus main.gd ausgezogen, Karte t_eaccbfed U1 — Logik unverändert): +## Menü Datei + Undo/Redo + Gitter/Höhenlinien + Symmetrie + Toolbar-Aktionen +## aus der Registry (Karte prüfen, ▶ Karte testen). Neue Aktionen melden sich +## künftig in registry.gd an (U2–U7, T1-F5 bleibt unverändert erreichbar). + +var undo_btn: Button +var redo_btn: Button +var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014) +var contours_btn: Button # „≈ Höhenlinien“ (Karte t_3ebe4086) +var sym_menu: MenuButton +var size_menu: MenuButton # „UI-Größe“ (Karte t_297221b5) +var title_label: Label +var file_label: Label + + +static func spec() -> Dictionary: + return {"titel": "Toolbar", "gruppe": "toolbar", "order": 0} + + +func build() -> void: + add_theme_constant_override("separation", UIScale.px(10)) + + var file_menu := MenuButton.new() + file_menu.text = "Datei" + file_menu.flat = false + file_menu.custom_minimum_size = UIScale.v2(90, 40) + var popup := file_menu.get_popup() + popup.add_item("Neue Map…", 0) + popup.add_item("Öffnen…", 1) + popup.add_item("Kartengröße ändern…", 6) + popup.add_separator() + popup.add_item("Speichern (Strg+S)", 2) + popup.add_item("Speichern unter…", 3) + popup.add_separator() + popup.add_item("▶ Karte testen", 5) + popup.add_separator() + popup.add_item("🔄 Updates prüfen", 7) + popup.add_item("Beenden", 4) + popup.id_pressed.connect(main.call.bind("_on_menu_action")) + add_child(file_menu) + + undo_btn = Button.new() + undo_btn.text = "↩ Rückgängig" + undo_btn.custom_minimum_size = UIScale.v2(140, 40) + undo_btn.tooltip_text = "Letzte Aktion zurücknehmen (Strg+Z)" + undo_btn.pressed.connect(main.call.bind("_do_undo")) + add_child(undo_btn) + + redo_btn = Button.new() + redo_btn.text = "↪ Wiederholen" + redo_btn.custom_minimum_size = UIScale.v2(150, 40) + redo_btn.tooltip_text = "Zurückgenommene Aktion wiederherstellen (Strg+Y)" + redo_btn.pressed.connect(main.call.bind("_do_redo")) + add_child(redo_btn) + + # Maßstabsgitter-Toggle (Karte t_7ac70014): über sichtbarem Terrain ist das + # Gitter standardmäßig AUS (das Terrain ist die Bodendarstellung); wer das + # 100-m-Raster als Planungshilfe will, schaltet es hier ohne Rebuild zu. + grid_btn = Button.new() + grid_btn.text = "⊞ Gitter" + grid_btn.toggle_mode = true + grid_btn.button_pressed = false + grid_btn.custom_minimum_size = UIScale.v2(120, 40) + grid_btn.tooltip_text = "Maßstabsgitter (100-m-Raster) im Viewport ein-/ausblenden. Über sichtbarem Terrain ist es standardmäßig aus." + grid_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_toggled", pressed)) + add_child(grid_btn) + + # Höhenlinien-Button (Karte t_3ebe4086, Kopplung t_89c0ec95): drücken = + # LINIEN FIXIEREN (immer sichtbar, in jedem Werkzeug). Ohne Fix sind die + # Linien NUR im Terrain-Werkzeug (F1) sichtbar; auf flachen Maps (< 2 m + # Gesamtrelief) schaltet das Terrain-Mesh zusätzlich selbst auf aus. + contours_btn = Button.new() + contours_btn.text = "≈ Höhenlinien" + contours_btn.toggle_mode = true + contours_btn.button_pressed = false + contours_btn.custom_minimum_size = UIScale.v2(150, 40) + contours_btn.tooltip_text = "Höhenlinien (1 m, alle 5 m kräftiger) FIXIERT an/aus. Ohne Fixierung erscheinen sie nur im Terrain-Werkzeug (F1). Taste L schaltet ebenfalls. Bei flachem Terrain (< 2 m) sind sie automatisch aus." + contours_btn.toggled.connect(func(pressed: bool) -> void: main.call("_on_contours_toggled", pressed)) + add_child(contours_btn) + + # Symmetrie-Schalter (Karte t_f5935023, M1): Aus / Punkt / Achse X / Achse Z. + # Standard = Punktspiegelung (Sly 20.10.). M1 setzt nur Modus + Anzeige; + # das gespiegelte Bauen selbst kommt mit M2–M4. + sym_menu = MenuButton.new() + sym_menu.text = "Symmetrie: Punkt" + sym_menu.flat = false + sym_menu.custom_minimum_size = UIScale.v2(160, 40) + sym_menu.tooltip_text = "Gespiegeltes Bauen: Änderungen werden automatisch auf die andere Kartenseite übertragen. Aus / Punkt (180° um die Kartenmitte) / Achse X / Achse Z. Standard: Punkt." + var sym_popup := sym_menu.get_popup() + for i in SymmetryCore.MODE_LABELS.size(): + sym_popup.add_check_item(SymmetryCore.MODE_LABELS[i], i) + sym_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_sym_mode_selected", index)) + add_child(sym_menu) + + # Toolbar-Aktionen DATENGETRIEBEN aus der Registry (Operator-Vorgabe + # 20.10.): Karte prüfen, ▶ Karte testen — künftige Karten melden sich + # hier an, ohne main.gd anzufassen. Die Buttons bekommen den Registry-Namen + # als Node-Namen (main kann sie per get_node/find_child ansprechen, z. B. + # der Shake-Effekt beim fehlgeschlagenen Map-Check). + for entry in PanelRegistry.toolbar_actions(): + var btn := Button.new() + btn.text = str(entry["titel"]) + btn.name = str(entry["id"]) + if str(entry["id"]) == "karte_pruefen": + btn.custom_minimum_size = UIScale.v2(170, 40) + btn.tooltip_text = "Prüft, ob die Map bereit für den Export ist — sagen wir VORHER, was fehlt" + else: + btn.custom_minimum_size = UIScale.v2(190, 40) + btn.tooltip_text = "Startet die Map SOFORT als Spiel zum Durchlaufen — eigenes Fenster, der Editor bleibt offen. ESC im Spiel = zurück zum Editor." + btn.pressed.connect(main.call.bind(str(entry["action"]))) + add_child(btn) + + # UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um, + # es wirkt SOFORT (kompletter UI-Neuaufbau mit dem neuen Faktor) und bleibt + # in user://scopedvr_editor.cfg gespeichert. + size_menu = MenuButton.new() + size_menu.text = "UI-Größe" + size_menu.flat = false + size_menu.custom_minimum_size = UIScale.v2(120, 40) + size_menu.tooltip_text = "Größe der Oberfläche: Klein (wie bisher), Mittel (1,25-fach), Groß (1,5-fach). Wirkt sofort und bleibt gespeichert." + var size_popup := size_menu.get_popup() + for i in UIScale.step_count(): + size_popup.add_check_item(UIScale.step_label(i), i) + # Häkchen-Sync macht der KOORDINATOR nach _pull_toolbar_refs (hier ist + # main.size_menu noch null — alter Reihenfolge-Bug, Karte t_eaccbfed). + size_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index)) + add_child(size_menu) + + title_label = Label.new() + title_label.text = " %s" % main.APP_TITLE + title_label.add_theme_font_size_override("font_size", UIScale.f(20)) + # Karte t_297221b5: bei großer Stufe darf der Titel EGALISIEREN statt die + # Top-Bar aus dem Fenster zu drücken (Mindestbreite der Schrift skaliert mit). + # Mindestbreite, damit er nicht KOMPLETT verschwindet (Ellipsis macht "Scop…"). + # Expand: bei Platz bekommt er seine VOLLSTÄNDIGE natürliche Breite (wie + # Vorher bei „Klein“), erst bei knappem Fenster egalisiert er mit "…". + title_label.custom_minimum_size = UIScale.v2(180, 0) + title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + title_label.clip_text = true + add_child(title_label) + + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + add_child(spacer) + + file_label = Label.new() + file_label.text = "" + file_label.modulate = Color(1, 1, 1, 0.6) + # Karte t_297221b5: Map-Pfad darf bei großer Stufe egalisiert werden + # (Voller Name im Tooltip, sonst drückt die Zeile die Top-Bar aus dem Fenster). + # Expand: bekommt zusammen mit dem Spacer seinen Anteil, egalisiert mit "…". + file_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + file_label.clip_text = true + file_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + add_child(file_label) diff --git a/src/ui/panels/toolbar.gd.uid b/src/ui/panels/toolbar.gd.uid new file mode 100644 index 0000000..9f1da55 --- /dev/null +++ b/src/ui/panels/toolbar.gd.uid @@ -0,0 +1 @@ +uid://po0n02xkbp2e diff --git a/tests/test_ui_layout.gd b/tests/test_ui_layout.gd new file mode 100644 index 0000000..ead3c30 --- /dev/null +++ b/tests/test_ui_layout.gd @@ -0,0 +1,155 @@ +extends SceneTree +## test_ui_layout (Karte t_eaccbfed, U1 UI-Fundament): prüft das Unity-artige +## Layout, das AUSSCHLIESSLICH aus der Registry (src/ui/panels/registry.gd) +## gebaut wird. +## +## Geprüft wird: +## 1. Alle Registry-Bereiche existieren GENAU EINMAL im Baum (kein Doppel, +## kein Verwaister) und als Tab mit dem Registry-Titel erreichbar sind. +## 2. Die Tab-Gruppen stimmen: rechts = 5 Tabs, links = 1, unten = 2. +## 3. Assets-Bereich unten vorhanden (Objekt-Bibliothek mit Suche + Kacheln). +## 4. Theme gesetzt (theme/editor_theme.tres als einzige Farbquelle). +## 5. Keine doppelten Node-Namen im UI-Subbaum. +## 6. main.gd hält die bekannten Werkzeug-/Test-Referenzen (Vertrag). + +const UIScaleStepDefault := 0 # (Lesbarkeit; Stufen sind nicht Teil dieser Suite) + +var _fails := 0 +var _oks := 0 + + +func _check(cond: bool, what: String) -> void: + if cond: + _oks += 1 + print(" ok: ", what) + else: + _fails += 1 + print(" FAIL: ", what) + + +func _init() -> void: + print("Testgruppe: Layout aus der Registry") + var scene: PackedScene = load("res://scenes/main.tscn") + var main: Control = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + await process_frame + + # --- 1. Registry-Tabelle ist die Autorität -------------------------------- + var registry_script: GDScript = load("res://src/ui/panels/registry.gd") + var ids: Array = [] + for entry in registry_script.PANELS: + ids.append(str(entry["id"])) + _check(ids.size() == registry_script.PANELS.size(), "Registry: IDs eindeutig (keine Duplikate in der Tabelle)") + _check(not ids.is_empty(), "Registry: mindestens ein Bereich angemeldet") + + # --- 2. Jeder Bereich GENAU EINMAL im Baum, als Tab mit Titel -------------- + var tabs_by_name: Dictionary = {} # gruppe → TabContainer + var stack: Array = [main] + var all_controls: Array = [] + while not stack.is_empty(): + var n: Node = stack.pop_back() + if n is TabContainer: + all_controls.append(n) + stack.append_array(n.get_children()) + for tab in all_controls: + for c in (tab as TabContainer).get_children(): + var cid := str(c.name) + if cid in ids: + _check(tabs_by_name.get(cid) == null, "Bereich '%s' GENAU EINMAL im Baum" % cid) + tabs_by_name[cid] = c + # Tab-Titel = Registry-Titel (erreichbar/erkennbar) + var title: String = "" + for entry in registry_script.PANELS: + if str(entry["id"]) == cid: + title = str(entry["titel"]) + var idx: int = c.get_index() + _check(tab.get_tab_title(idx) == title, + "Tab '%s' trägt Registry-Titel '%s'" % [cid, title]) + for wanted in ids: + _check(tabs_by_name.get(wanted) != null, "Bereich '%s' aus der Registry im Baum" % wanted) + + # --- 3. Tab-Gruppen -------------------------------------------------------- + var right: TabContainer = main.get("ui_right_panel") + var left: TabContainer = main.get("ui_left_panel") + var bottom: Node = main.get("ui_dock_bottom") + _check(right != null and right is TabContainer, "rechte Spalte ist ein TabContainer") + _check(left != null and left is TabContainer, "linke Spalte ist ein TabContainer (U2-vorbereitet)") + _check(bottom != null and bottom is VSplitContainer, "unten: VSplit (größenverstellbar)") + if right != null: + _check(right.get_child_count() == 5, "rechts: 5 Tabs (Inspektor/Auswahl/Map/Terrain/Prüfen) — hat %d" % right.get_child_count()) + _check(right.get_tab_count() == right.get_child_count(), + "rechts: alle Bereiche als Tab in der Leiste erreichbar (Titel je Bereich)") + for i in right.get_tab_count(): + _check(not str(right.get_tab_title(i)).is_empty(), + "rechts: Tab %d hat sichtbaren Titel" % i) + if left != null: + _check(left.get_child_count() == 1, "links: 1 Tab (Map-Inhalt) — hat %d" % left.get_child_count()) + if bottom is VSplitContainer and bottom.get_child_count() == 2: + var bchild: Node = bottom.get_child(1) # PanelContainer mit BottomTabs + var btab: TabContainer = null + for gc in bchild.get_children(): + if gc is TabContainer: + btab = gc + _check(btab != null, "unten: TabContainer für Assets gefunden") + if btab != null: + _check(btab.get_child_count() == 2, "unten: 2 Tabs (Bibliothek/Werkzeuge) — hat %d" % btab.get_child_count()) + + # --- 4. Assets-Bereich unten vollständig ----------------------------------- + var assets: Control = tabs_by_name.get("assets") + _check(assets != null, "Objekt-Bibliothek (Assets) unten vorhanden") + if assets != null: + _check(assets.get("library_search") != null, "Bibliothek: Suchfeld vorhanden") + _check(assets.get("library_open_all_btn") != null and assets.get("library_close_all_btn") != null, + "Bibliothek: „Alle auf/zu“ vorhanden") + var lib_box: Node = assets.get("library_box") + _check(lib_box != null and lib_box.get_child_count() > 0, "Bibliothek: Kachel-/Gruppen-Liste gefüllt") + var tools: Control = tabs_by_name.get("werkzeuge") + _check(tools != null and tools.get("grid_check") != null, "Objekt-Werkzeuge: Raster-Snap erhalten (nichts verschwindet)") + + # --- 5. Theme als einzige Farbquelle ---------------------------------------- + var theme_res: Theme = main.get("theme") + _check(theme_res != null, "Theme gesetzt (main.theme)") + if theme_res != null: + _check(int(theme_res.default_font_size) >= 14, "Theme: Grundschrift ≥ 14 px (Lesbarkeits-Grundsatz)") + var theme_file: Theme = load("res://theme/editor_theme.tres") + _check(theme_res == theme_file, "Theme: main nutzt theme/editor_theme.tres (eine Quelle)") + + # --- 6. Keine doppelten Node-Namen unter main ------------------------------- + var names: Dictionary = {} + var dupes: Array = [] + var stack2: Array = [main] + while not stack2.is_empty(): + var n2: Node = stack2.pop_back() + var key := str(n2.name) + "#" + n2.get_class() + if key in names: + dupes.append(str(n2.name)) + names[key] = true + stack2.append_array(n2.get_children()) + _check(dupes.is_empty(), "keine doppelten Node-Namen im UI — %s" % str(dupes)) + + # --- 7. Koordinator-Vertrag (Werkzeuge/Tests greifen weiter auf main.*) ---- + for ref_name in ["toolbar_mod", "status_mod", "selection_mod", "map_mod", + "terrain_mod", "check_mod", "tree_mod", "assets_mod", + "content_tree", "status_label", "prop_name", "library_search", + "terrain_radius_slider", "terrain_size_btn_medium"]: + _check(main.get(ref_name) != null, "main.%s vorhanden" % ref_name) + + # --- 8. Lauf-Modus (T1) bleibt erreichbar: Toolbar-Aktion aus Registry ------ + var tb: Control = main.get("toolbar_mod") + var found_test_btn := false + if tb != null: + for entry in registry_script.ACTIONS: + if str(entry["id"]) == "karte_testen": + var btn: Node = tb.find_child(str(entry["id"]), true, false) + found_test_btn = btn != null + _check(found_test_btn, "Lauf-Modus: „▶ Karte testen“ in der Toolbar (Registry-Aktion)") + + print("CHECKS: %d FAILURES: %d" % [_oks + _fails, _fails]) + if _fails > 0: + print("UI_LAYOUT_TEST_FAIL") + quit(1) + else: + print("ALL OK: %d checks" % (_oks + _fails)) + quit(0) \ No newline at end of file diff --git a/tests/test_ui_layout.gd.uid b/tests/test_ui_layout.gd.uid new file mode 100644 index 0000000..7876f06 --- /dev/null +++ b/tests/test_ui_layout.gd.uid @@ -0,0 +1 @@ +uid://dicjmgf78vxw8 diff --git a/theme/editor_theme.tres b/theme/editor_theme.tres new file mode 100644 index 0000000..f8f1559 --- /dev/null +++ b/theme/editor_theme.tres @@ -0,0 +1,124 @@ +[gd_resource type="Theme" load_steps=10 format=3] + +[sub_resource type="StyleBoxFlat" id="panel"] +bg_color = Color(0.12, 0.14, 0.17, 1) +border_color = Color(0.3, 0.36, 0.42, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[sub_resource type="StyleBoxFlat" id="tab_sel"] +bg_color = Color(0.24, 0.3, 0.36, 1) +border_color = Color(0.35, 0.55, 0.7, 1) +border_width_top = 2 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +content_margin_left = 10.0 +content_margin_right = 10.0 +content_margin_top = 4.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxFlat" id="tab_unsel"] +bg_color = Color(0.15, 0.18, 0.21, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +content_margin_left = 10.0 +content_margin_right = 10.0 +content_margin_top = 4.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxFlat" id="tabbar"] +bg_color = Color(0.1, 0.12, 0.14, 1) + +[sub_resource type="StyleBoxFlat" id="btn_normal"] +bg_color = Color(0.19, 0.23, 0.27, 1) +border_color = Color(0.3, 0.36, 0.42, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 +content_margin_left = 8.0 +content_margin_right = 8.0 + +[sub_resource type="StyleBoxFlat" id="btn_hover"] +bg_color = Color(0.25, 0.31, 0.37, 1) +border_color = Color(0.35, 0.55, 0.7, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 +content_margin_left = 8.0 +content_margin_right = 8.0 + +[sub_resource type="StyleBoxFlat" id="btn_pressed"] +bg_color = Color(0.14, 0.19, 0.24, 1) +border_color = Color(0.35, 0.55, 0.7, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 +content_margin_left = 8.0 +content_margin_right = 8.0 + +[sub_resource type="StyleBoxFlat" id="tooltip"] +bg_color = Color(0.08, 0.09, 0.11, 0.96) +border_color = Color(0.35, 0.55, 0.7, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 +content_margin_left = 6.0 +content_margin_right = 6.0 +content_margin_top = 4.0 +content_margin_bottom = 4.0 + +[sub_resource type="StyleBoxEmpty" id="focus_empty"] + +[resource] +default_font_size = 16 +Editor/colors/accent = Color(0.55, 0.8, 1, 1) +Editor/colors/status_ok = Color(0.75, 0.95, 0.75, 1) +Editor/colors/status_error = Color(1, 0.6, 0.5, 1) +Editor/colors/muted = Color(1, 1, 1, 0.55) +Editor/colors/field = Color(1, 1, 1, 0.7) +Editor/colors/title = Color(1, 1, 1, 0.6) +Editor/colors/help_bg = Color(0.09, 0.11, 0.13, 0.97) +Editor/colors/help_border = Color(0.35, 0.55, 0.7, 1) +Editor/colors/update_bg = Color(0.36, 0.25, 0.06, 0.97) +Editor/colors/update_border = Color(0.95, 0.72, 0.15, 1) +Editor/colors/rubber_fill = Color(0.45, 0.8, 1, 0.16) +Editor/colors/rubber_line = Color(0.45, 0.8, 1, 0.9) +PanelContainer/styles/panel = SubResource("panel") +TabContainer/styles/panel = SubResource("panel") +TabContainer/styles/tab_selected = SubResource("tab_sel") +TabContainer/styles/tab_unselected = SubResource("tab_unsel") +TabContainer/styles/tabbar_background = SubResource("tabbar") +TabContainer/colors/font_selected_color = Color(1, 1, 1, 1) +TabContainer/colors/font_unselected_color = Color(1, 1, 1, 0.7) +Button/styles/normal = SubResource("btn_normal") +Button/styles/hover = SubResource("btn_hover") +Button/styles/pressed = SubResource("btn_pressed") +Button/styles/focus = SubResource("focus_empty") +TooltipPanel/styles/panel = SubResource("tooltip") +TooltipLabel/colors/font_color = Color(1, 1, 1, 1) diff --git a/tools/run_suites.py b/tools/run_suites.py new file mode 100644 index 0000000..8076570 --- /dev/null +++ b/tools/run_suites.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Run all Godot -s test suites and summarize (Karte t_eaccbfed).""" +import subprocess, re, sys + +GODOT = "/opt/data/home/bin/godot4" +suites = sorted(__import__("glob").glob("tests/test_*.gd")) + +results = [] +for path in suites: + name = path.split("/")[-1][:-3] + proc = subprocess.run([GODOT, "--headless", "--path", ".", "-s", path], + capture_output=True, text=True, timeout=600) + out = proc.stdout + proc.stderr + if re.search(r"SCRIPT ERROR:|Parse Error", out): + st = "SCRIPT_ERROR" + elif re.search(r"FAILURES: [1-9]|FAILED|TEST_FAIL", out): + st = "FAIL" + else: + st = "OK" + summary = "" + for line in out.splitlines(): + if re.search(r"ALL OK|CHECKS:|FAILED:", line): + summary = line.strip() + results.append((name, st, summary)) + print(f"{name}: {st} | {summary}", flush=True) + +ok = sum(1 for _, s, _ in results if s == "OK") +print(f"\nSUITEN_TOTAL={len(results)} GRUEN={ok} ROT={len(results)-ok}") +sys.exit(0 if ok == len(results) else 1) \ No newline at end of file