t_eaccbfed: U1 UI-Fundament (Operator-Push) — main.gd 3677->2479 Zeilen, src/ui/panels/ (11 Module + registry), Tabs rechts+links, Assets unten, theme/editor_theme.tres, tests/test_ui_layout.gd (63 checks). Worker meldete fertig ohne Push; Arbeit aus dem Scratch-Ordner gerettet.
This commit is contained in:
parent
a5d8f7f79d
commit
69ffe23a71
27 changed files with 2341 additions and 1611 deletions
2024
src/ui/main.gd
2024
src/ui/main.gd
File diff suppressed because it is too large
Load diff
278
src/ui/panels/assets_panel.gd
Normal file
278
src/ui/panels/assets_panel.gd
Normal file
|
|
@ -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)
|
||||||
1
src/ui/panels/assets_panel.gd.uid
Normal file
1
src/ui/panels/assets_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dxoh6gjklwjna
|
||||||
146
src/ui/panels/check_panel.gd
Normal file
146
src/ui/panels/check_panel.gd
Normal file
|
|
@ -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)
|
||||||
1
src/ui/panels/check_panel.gd.uid
Normal file
1
src/ui/panels/check_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b84fxjtuhejui
|
||||||
53
src/ui/panels/editor_panel.gd
Normal file
53
src/ui/panels/editor_panel.gd
Normal file
|
|
@ -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()
|
||||||
1
src/ui/panels/editor_panel.gd.uid
Normal file
1
src/ui/panels/editor_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cu0oq4cx0oy60
|
||||||
127
src/ui/panels/map_panel.gd
Normal file
127
src/ui/panels/map_panel.gd
Normal file
|
|
@ -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
|
||||||
1
src/ui/panels/map_panel.gd.uid
Normal file
1
src/ui/panels/map_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dihu04ppa406d
|
||||||
34
src/ui/panels/object_tools_panel.gd
Normal file
34
src/ui/panels/object_tools_panel.gd
Normal file
|
|
@ -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."))
|
||||||
1
src/ui/panels/object_tools_panel.gd.uid
Normal file
1
src/ui/panels/object_tools_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cs4xao3sf5y7n
|
||||||
87
src/ui/panels/registry.gd
Normal file
87
src/ui/panels/registry.gd
Normal file
|
|
@ -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 {}
|
||||||
1
src/ui/panels/registry.gd.uid
Normal file
1
src/ui/panels/registry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cmom62lxsqgef
|
||||||
155
src/ui/panels/scene_tree_panel.gd
Normal file
155
src/ui/panels/scene_tree_panel.gd
Normal file
|
|
@ -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)
|
||||||
1
src/ui/panels/scene_tree_panel.gd.uid
Normal file
1
src/ui/panels/scene_tree_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bpbw3vo36ohv1
|
||||||
234
src/ui/panels/selection_panel.gd
Normal file
234
src/ui/panels/selection_panel.gd
Normal file
|
|
@ -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])
|
||||||
1
src/ui/panels/selection_panel.gd.uid
Normal file
1
src/ui/panels/selection_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dwp3df5nsfl3s
|
||||||
58
src/ui/panels/status_bar.gd
Normal file
58
src/ui/panels/status_bar.gd
Normal file
|
|
@ -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)
|
||||||
1
src/ui/panels/status_bar.gd.uid
Normal file
1
src/ui/panels/status_bar.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dddgvtqqr3lau
|
||||||
279
src/ui/panels/terrain_panel.gd
Normal file
279
src/ui/panels/terrain_panel.gd
Normal file
|
|
@ -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))
|
||||||
1
src/ui/panels/terrain_panel.gd.uid
Normal file
1
src/ui/panels/terrain_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://qvp3thn60awa
|
||||||
157
src/ui/panels/toolbar.gd
Normal file
157
src/ui/panels/toolbar.gd
Normal file
|
|
@ -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)
|
||||||
1
src/ui/panels/toolbar.gd.uid
Normal file
1
src/ui/panels/toolbar.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://po0n02xkbp2e
|
||||||
155
tests/test_ui_layout.gd
Normal file
155
tests/test_ui_layout.gd
Normal file
|
|
@ -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)
|
||||||
1
tests/test_ui_layout.gd.uid
Normal file
1
tests/test_ui_layout.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dicjmgf78vxw8
|
||||||
124
theme/editor_theme.tres
Normal file
124
theme/editor_theme.tres
Normal file
|
|
@ -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)
|
||||||
29
tools/run_suites.py
Normal file
29
tools/run_suites.py
Normal file
|
|
@ -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)
|
||||||
Loading…
Reference in a new issue