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

590 lines
24 KiB
GDScript

extends "res://core/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 group_btn: Button
var ungroup_btn: Button
var _tree_select_lock := false # verhindert Rückkopplung Auswahl ↔ Baum
var _tree_select_pending := false # multi_selected gesammelt, Auswertung steht aus
var _obj_index_map: Dictionary = {} # obj_index -> TreeItem (schnelle Zwei-Wege-Kopplung)
# U3 (Karte t_a4bf9b8c): Kontextmenü, Umbenennen, Lösch-Rückfrage, Drag&Drop.
var _context_menu: PopupMenu
var _rename_dialog: AcceptDialog
var _rename_field: LineEdit
var _rename_gid := -1 # Gruppe, die das Umbenennen-Dialogfeld betrifft
var _confirm_delete: ConfirmationDialog
var _confirm_gid := -1 # Gruppe, die das Lösch-Rückfragen betrifft
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
# FIX 25.10. (Test-Befund „Liste anklicken wählt in der Szene nichts aus“):
# Im Modus SELECT_MULTI feuert Godot NIE item_selected, sondern nur
# multi_selected (pro Zeile). Beide anschließen; das Sammeln übernimmt der
# Aufrufer-Merker, damit ein Klick mit mehreren Signalen nur EINMAL wirkt.
content_tree.item_selected.connect(_on_tree_selected)
content_tree.multi_selected.connect(_on_tree_multi_selected)
add_child(content_tree)
# Gruppen verwalten (25.10.: der Tab „Auswahl“ ist weg): mehrere Objekte in der
# Liste (Strg+Klick) oder in der Szene markieren, dann hier gruppieren/lösen.
# Rechtsklick auf eine Zeile bietet dieselben Aktionen.
var group_row := HBoxContainer.new()
group_row.add_theme_constant_override("separation", UIScale.px(8))
add_child(group_row)
group_btn = Button.new()
group_btn.text = "🔗 Gruppieren"
group_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
group_btn.custom_minimum_size = UIScale.v2(0, 40)
group_btn.tooltip_text = "Markierte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam. Mehrere markieren: Strg+Klick in der Liste oder Rahmen in der Szene."
group_btn.pressed.connect(func() -> void: main.interaction.group_selection())
group_row.add_child(group_btn)
ungroup_btn = Button.new()
ungroup_btn.text = "⛓ Gruppe lösen"
ungroup_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
ungroup_btn.custom_minimum_size = UIScale.v2(0, 40)
ungroup_btn.tooltip_text = "Die Gruppe der Auswahl auflösen (Strg+Umschalt+G) — die Objekte bleiben, nur die Bindung löst sich"
ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
group_row.add_child(ungroup_btn)
# U3 (Karte t_a4bf9b8c): Rechtsklick-Kontextmenü + Drag&Drop für Gruppen.
content_tree.gui_input.connect(_on_tree_gui_input)
# Tree.set_drag_forwarding: die drei Virtuals (_get_drag_data/_can_drop_data/
# _drop_data) wohnen am Tree selbst — hier per Callables weitergeleitet.
content_tree.set_drag_forwarding(_tree_drag_data, _tree_can_drop, _tree_drop)
_context_menu = PopupMenu.new()
_context_menu.hide_on_item_selection = true
_context_menu.id_pressed.connect(_on_menu_action)
add_child(_context_menu)
_rename_dialog = AcceptDialog.new()
_rename_dialog.title = "Gruppe umbenennen"
_rename_dialog.ok_button_text = "Übernehmen"
# Hinweis: AcceptDialog kennt in Godot 4.7 weder cancel_button_text noch
# get_cancel_button() — der Default-Abbrechen-Button bleibt, wie er ist.
_rename_field = LineEdit.new()
_rename_field.placeholder_text = "Gruppenname"
_rename_dialog.add_child(_rename_field)
_rename_dialog.confirmed.connect(_on_rename_confirmed)
add_child(_rename_dialog)
_confirm_delete = ConfirmationDialog.new()
_confirm_delete.title = "Gruppe löschen"
_confirm_delete.ok_button_text = "Löschen"
_confirm_delete.cancel_button_text = "Abbrechen"
_confirm_delete.confirmed.connect(_on_delete_confirmed)
add_child(_confirm_delete)
add_child(_separator())
## multi_selected kommt je Zeile (auch beim Abwählen): die Auswahl EINMAL pro
## Frame auswerten, nicht pro Signal.
func _on_tree_multi_selected(_item: TreeItem, _column: int, _selected: bool) -> void:
if _tree_select_lock or _tree_select_pending:
return
_tree_select_pending = true
_apply_tree_selection.call_deferred()
func _apply_tree_selection() -> void:
_tree_select_pending = false
_on_tree_selected()
## 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
# Gruppe geklickt (Karte t_e3c0310a U2): alle Mitglieder auswählen.
if item.has_meta("group_id"):
var g_indices: Array = []
var walk_g := item.get_first_child()
while walk_g != null:
if walk_g.has_meta("obj_index"):
g_indices.append(int(walk_g.get_meta("obj_index")))
walk_g = walk_g.get_next()
if not g_indices.is_empty():
_tree_select_lock = true
main.interaction.select_many(g_indices)
_tree_select_lock = false
main.call("_set_status", "Gruppe %d ausgewählt (%d Objekte) — gemeinsam verschieben/drehen/duplizieren." % [int(item.get_meta("group_id")), g_indices.size()])
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())
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
# Zwei-Wege-Kopplung (Karte t_e3c0310a U2): die Auswahl in 3D spiegelt
# sich im Baum — über die Index-Karte O(1) statt Voll-Suche.
for idx in wanted.keys():
if _obj_index_map.has(idx):
var item: TreeItem = _obj_index_map[idx]
item.select(0)
# Abwählen: alle übrigen markierten Objekt-Zeilen deselektieren.
var walk := content_tree.get_next_selected(null)
while walk != null:
if walk.has_meta("obj_index") and not wanted.has(int(walk.get_meta("obj_index"))):
walk.deselect(0)
walk = content_tree.get_next_selected(walk)
var primary: Variant = main.interaction.get("selected_index") if main.interaction != null else -1
if primary is int and primary >= 0 and _obj_index_map.has(primary):
_ensure_tree_visible(content_tree.get_root(), primary)
_tree_select_lock = false
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()
_obj_index_map.clear()
var root := content_tree.create_item()
var objects: Array = main.doc.get("objects", [])
var zones: Dictionary = main.doc.get("spawn_zones", {})
var group_ids: Array = GroupOps.active_group_ids(main.doc)
# Leerer Zustand (Karte t_e3c0310a U2): Klartext-Hinweis statt leere Gruppen.
if objects.is_empty() and zones.is_empty() and group_ids.is_empty():
var empty_item := content_tree.create_item(root)
empty_item.set_text(0, "Noch keine Objekte — links in der Objekt-Bibliothek wählen")
empty_item.set_selectable(0, false)
return
# Objekte: Kategorie → Objekte (Karte t_e3c0310a U2 — z. B. Mauern, Barrieren).
var obj_item := content_tree.create_item(root)
obj_item.set_text(0, "Objekte (%d)" % objects.size())
obj_item.set_meta("objekte_kopf", true) # Drag-Ziel „aus Gruppe herauslösen“ (U3)
var kat_map: Dictionary = {}
for i in objects.size():
var obj = objects[i]
if obj is Dictionary:
var oid := str(obj.get("id", "?"))
var kat := LibraryBrowser.group_of(oid)
if not kat_map.has(kat):
kat_map[kat] = []
kat_map[kat].append({"idx": i, "obj": obj})
for kat in kat_map.keys():
var kat_item := content_tree.create_item(obj_item)
kat_item.set_text(0, "%s (%d)" % [_kat_label(kat), (kat_map[kat] as Array).size()])
for data in kat_map[kat]:
var idx: int = int(data["idx"])
var obj: Dictionary = data["obj"]
var is_visible: bool = bool(obj.get("visible", true))
var item := content_tree.create_item(kat_item)
item.set_text(0, ("👁 " if is_visible else "🚫 ") + "%s #%d" % [main.object_label(str(obj.get("id", "?"))), idx + 1])
item.set_meta("obj_index", idx)
item.set_meta("visible", is_visible)
_obj_index_map[idx] = item
# Spawn-Zonen: Team A/B (Karte t_e3c0310a U2 — aus der alten Struktur).
if not zones.is_empty():
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])])
# Gruppen (Karte t_e3c0310a U2): aktive Gruppen als eigene Ebene im Baum.
# Klick auf eine Gruppe wählt ALLE Mitglieder in der 3D-Szene (Zwei-Wege).
if not group_ids.is_empty():
var grp_item := content_tree.create_item(root)
grp_item.set_text(0, "Gruppen (%d)" % group_ids.size())
grp_item.set_meta("objekte_kopf", true) # Drag-Ziel „herauslösen“ (U3)
for gid in group_ids:
var member_indices: Array = GroupOps.members_of(main.doc, gid)
# U3 (Karte t_a4bf9b8c): gespeicherter Name voran, Fallback "Gruppe N".
var g_node := content_tree.create_item(grp_item)
g_node.set_text(0, "%s (%d Objekte)" % [GroupState.display_name(main.doc, int(gid)), member_indices.size()])
g_node.set_meta("group_id", int(gid))
for mi in member_indices:
var obj: Dictionary = main.doc["objects"][int(mi)]
var m_item := content_tree.create_item(g_node)
m_item.set_text(0, " %s" % main.object_label(str(obj.get("id", "?"))))
m_item.set_meta("obj_index", int(mi))
## Kategorie-Schlüssel (z. B. "mauern") in Anzeige-Label (z. B. "Mauern") —
## Karte t_e3c0310a U2. Nutzt KAT_GROUPS aus LibraryBrowser.
static func _kat_label(key: String) -> String:
for kat_name in LibraryBrowser.KAT_GROUPS:
var g: Dictionary = LibraryBrowser.KAT_GROUPS[kat_name]
if str(g["key"]) == key:
return str(g["label"])
return key.capitalize()
# ================= U3: Gruppen im Baum (Karte t_a4bf9b8c) ====================
# Gruppen direkt im Szenen-Baum bilden: Rechtsklick-Menü (Gruppieren/Auflösen/
# Umbenennen/Löschen), Drag&Drop für die Mitgliedschaft, Gruppennamen im
# Dokument (GroupState, additiv). Jede Aktion = EIN Undo-Schritt über
# main.history — die Mehrfachauswahl-Logik (interaction.gd) wird benutzt,
# nicht kopiert; object_ops.gd nur LESEND.
## Rechtsklick im Baum: Zeile unter der Maus finden → Kontextmenü aufmachen.
func _on_tree_gui_input(event: InputEvent) -> void:
if not (event is InputEventMouseButton):
return
var mb := event as InputEventMouseButton
if not mb.pressed or mb.button_index != MOUSE_BUTTON_RIGHT:
return
var item: TreeItem = content_tree.get_item_at_position(mb.position)
if item == null:
return
# Rechtsklick auf eine (noch nicht markierte) Zeile wählt sie an — wie in
# Unity/Godot: das Menü gehört zur angeklickten Zeile. Direkter Ruf von
# _on_tree_selected (item_selected feuert bei .select() nicht selbst).
if item.has_meta("obj_index") and not item.is_selected(0):
content_tree.deselect_all()
item.select(0)
_on_tree_selected()
_open_context_menu(item, Vector2i(content_tree.get_screen_transform() * mb.position))
## Kontextmenü je Zeilen-Typ aufbauen (Klartext-Einträge, je Aktion eine ID).
func _open_context_menu(item: TreeItem, at_screen: Vector2i) -> void:
_context_menu.clear()
var has_entries := false
# Gruppen-Knoten: Auflösen / Umbenennen / Löschen (Rückfrage bei > 1 Objekt).
if item.has_meta("group_id"):
var gid := int(item.get_meta("group_id"))
_context_menu.add_item("⛓ Gruppe auflösen", 1)
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loesen_gruppe", "gid": gid})
_context_menu.add_item("✂ Umbenennen…", 2)
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "umbenennen", "gid": gid})
_context_menu.add_item("🗑 Gruppe löschen…", 3)
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loeschen_gruppe", "gid": gid})
has_entries = true
# Objekt-Zeile: Gruppieren (wenn ≥ 2 markiert), Aus Gruppe lösen, Löschen.
if item.has_meta("obj_index"):
var sel: Array = main.interaction.selected_indices
if sel.size() >= 2:
_context_menu.add_item("🔗 Gruppieren (%d Objekte)" % sel.size(), 10)
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "gruppieren"})
has_entries = true
var objects: Array = main.doc.get("objects", [])
var idx := int(item.get_meta("obj_index"))
if idx >= 0 and idx < objects.size() and GroupOps.group_id(objects[idx]) != GroupOps.UNGROUPED:
_context_menu.add_item("⛓ Aus Gruppe lösen", 1)
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loesen_einzeln", "idx": idx})
has_entries = true
_context_menu.add_item("🗑 Löschen", 3)
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loeschen_einzeln", "idx": idx})
has_entries = true
if not has_entries:
return
_context_menu.position = at_screen
_context_menu.popup()
## Menü-Auswahl → Aktion (Metadaten tragen Ziel; ID → Zeile über get_item_index,
## robust auch nachdem das Menü sich bei hide_on_item_selection schon versteckt).
func _on_menu_action(id: int) -> void:
var menu_index := _context_menu.get_item_index(id)
if menu_index < 0:
return
var meta: Variant = _context_menu.get_item_metadata(menu_index)
if not (meta is Dictionary):
return
var m := meta as Dictionary
match str(m.get("aktion", "")):
"gruppieren":
group_selected()
"loesen_gruppe":
ungroup_group(int(m.get("gid", -1)))
"loesen_einzeln":
ungroup_indices([int(m.get("idx", -1))])
"umbenennen":
open_rename_dialog(int(m.get("gid", -1)))
"loeschen_gruppe":
delete_group(int(m.get("gid", -1)))
"loeschen_einzeln":
_select_for_action([int(m.get("idx", -1))])
main.interaction.delete_selected()
## Auswahl für eine Aktion setzen (Baum-Markierung + 3D synchron).
func _select_for_action(indices: Array) -> void:
_tree_select_lock = true
main.interaction.select_many(indices)
_tree_select_lock = false
## Mehrfachauswahl → Gruppe (nutzt die bestehende interaction-Logik, EIN Undo).
func group_selected() -> void:
var sel: Array = main.interaction.selected_indices
if sel.size() < 2:
main.call("_set_status", "Gruppieren braucht mindestens 2 markierte Objekte — Strg+Klick im Baum oder im Viewport.", true)
return
main.interaction.group_selection()
_refresh_after_group_change()
_select_for_action(sel) # Gruppe bleibt markiert (Mitglieder hängen jetzt unter dem Gruppen-Knoten)
## GANZE Gruppe auflösen: Mitglieder anwählen → bestehende Löse-Logik.
func ungroup_group(gid: int) -> void:
var members: Array = GroupOps.members_of(main.doc, gid)
if members.is_empty():
main.call("_set_status", "Gruppe %d existiert nicht mehr." % (gid + 1), true)
return
_select_for_action(members)
main.interaction.ungroup_selection()
_refresh_after_group_change()
## Objekte aus ihrer Gruppe herauslösen (NUR diese Indizes — Drag-out-Pfad).
## EIN Undo-Schritt: snapshot ["objects", "group_names"] vor/nach (verwaiste
## Namen-Einträge werden im selben Schritt mitgeräumt).
func ungroup_indices(indices: Array) -> void:
var before := _snapshot_group_blocks()
var freed := GroupState.clear_group_fields(main.doc, indices)
if freed == 0:
main.call("_set_status", "Die markierten Objekte stecken in keiner Gruppe.")
return
GroupState.prune_orphans(main.doc)
main.history.commit("Objekt aus Gruppe gelöst (%d)" % freed,
before, _snapshot_group_blocks())
main.call("_refresh_history_buttons")
_refresh_after_group_change()
## Snapshot für Gruppen-Undo-Schritte: ["objects", "group_names"]. Fehlt der
## Namen-Block im Dokument (alte Maps), wird er als null SEEDED — ActionHistory
## wendet nur Schlüssel an, die im Snapshot stehen; ohne Seed bliebe der Name
## nach Undo als Leiche stehen (Restore von null löscht den Block sauber).
func _snapshot_group_blocks() -> Dictionary:
var out: Dictionary = main.history.snapshot(["objects", "group_names"])
if not out.has(GroupState.NAMES_KEY):
out[GroupState.NAMES_KEY] = null
return out
## Umbenennen-Kern (testbar ohne Dialog): Name im Dokument setzen, EIN Undo.
func rename_group(gid: int, new_name: String) -> bool:
if gid < 0:
return false
var clean := new_name.strip_edges()
if GroupState.group_name(main.doc, gid) == clean:
return false # Name unverändert — kein leerer Undo-Eintrag
var before := _snapshot_group_blocks()
GroupState.set_group_name(main.doc, gid, new_name)
GroupState.prune_orphans(main.doc)
var after := _snapshot_group_blocks()
var label := ("Gruppe umbenannt in „%s“" % clean) if not clean.is_empty() \
else ("Gruppenname entfernt (Gruppe %d)" % (gid + 1))
main.history.commit(label, before, after)
main.call("_refresh_history_buttons")
_refresh_after_group_change()
return true
## Umbenennen-Dialog (Rechtsklick am Gruppen-Knoten).
func open_rename_dialog(gid: int) -> void:
var members: Array = GroupOps.members_of(main.doc, gid)
if members.is_empty():
main.call("_set_status", "Gruppe %d existiert nicht mehr." % (gid + 1), true)
return
_rename_gid = gid
_rename_field.text = GroupState.group_name(main.doc, gid)
_rename_dialog.dialog_text = "Neuer Name für die Gruppe (leer = wieder „Gruppe %d“):" % (gid + 1)
_rename_field.grab_focus()
_rename_dialog.popup_centered(Vector2i(360, 100))
func _on_rename_confirmed() -> void:
if _rename_gid >= 0:
rename_group(_rename_gid, _rename_field.text)
_rename_gid = -1
## Gruppe löschen: Rückfrage bei > 1 Objekt, dann bestehende Lösch-Logik
## (interaction.delete_selected — EIN Undo-Schritt, Marker/3D räumen mit auf).
func delete_group(gid: int) -> void:
var members: Array = GroupOps.members_of(main.doc, gid)
if members.is_empty():
main.call("_set_status", "Gruppe %d existiert nicht mehr." % (gid + 1), true)
return
if members.size() > 1:
_confirm_gid = gid
_confirm_delete.dialog_text = "Gruppe „%s“ löschen? Die %d Objekte darin werden ALLE gelöscht — Strg+Z holt sie zurück." % [GroupState.display_name(main.doc, gid), members.size()]
_confirm_delete.popup_centered(Vector2i(480, 140))
return
delete_group_confirmed(gid)
## Lösch-Kern NACH der Rückfrage (auch direkt aufrufbar — Tests).
func delete_group_confirmed(gid: int) -> void:
var members: Array = GroupOps.members_of(main.doc, gid)
if members.is_empty():
return
_select_for_action(members)
main.interaction.delete_selected() # ruft selbst _after_structure_change → Baum neu
func _on_delete_confirmed() -> void:
if _confirm_gid >= 0:
delete_group_confirmed(_confirm_gid)
_confirm_gid = -1
# --- Drag & Drop (Mitgliedschaft ändern) -------------------------------------
## Zieh-Menge bestimmen: gezogene Zeile ist Teil der Markierung → die GANZE
## Markierung wandert mit; sonst nur die gezogene Zeile. (Testbar ohne echten
## Drag — _tree_drag_data ruft nur dies + set_drag_preview.)
func _collect_drag_indices(item: TreeItem) -> Array:
if item == null or not item.has_meta("obj_index"):
return []
var idx := int(item.get_meta("obj_index"))
var sel: Array = main.interaction.selected_indices
if idx in sel:
var out: Array = []
for i in sel:
if i is int:
out.append(int(i))
return out
return [idx]
## Tree-Drag-Start (set_drag_forwarding-Callable): Datenpaket für Drop-Ziele.
## Vorschau-Control wird NICHT in den Baum gehängt (set_drag_preview verlangt
## ein Control außerhalb des Baums).
func _tree_drag_data(from_position: Vector2) -> Variant:
var item: TreeItem = content_tree.get_item_at_position(from_position)
var indices := _collect_drag_indices(item)
if indices.is_empty():
return null
var label := Label.new()
label.text = "%d Objekt(e) ziehen" % indices.size()
content_tree.set_drag_preview(label)
return {"svmap_tree_objekte": indices}
## Drop erlaubt: AUF einen Gruppen-Knoten (aufnehmen) oder AUF den Objekte-Kopf/
## eine Kategorie-Zeile (herauslösen — nur wenn mindestens ein gezogenes Objekt
## überhaupt gruppiert ist).
func _tree_can_drop(at_position: Vector2, data: Variant) -> bool:
if not (data is Dictionary) or not (data as Dictionary).has("svmap_tree_objekte"):
return false
var item: TreeItem = content_tree.get_item_at_position(at_position)
if item == null:
return false
if item.has_meta("group_id"):
return true
if item.has_meta("objekte_kopf"):
var objects: Array = main.doc.get("objects", [])
for i in (data["svmap_tree_objekte"] as Array):
if i is int and i >= 0 and i < objects.size() \
and GroupOps.group_id(objects[i]) != GroupOps.UNGROUPED:
return true
return false
## Drop ausführen → Mitgliedschaft ändern (EIN Undo-Schritt), Baum/Auswahl neu.
func _tree_drop(at_position: Vector2, data: Variant) -> void:
var item: TreeItem = content_tree.get_item_at_position(at_position)
if item == null:
return
var indices: Array = (data as Dictionary).get("svmap_tree_objekte", [])
if item.has_meta("group_id"):
_apply_membership_drop(indices, int(item.get_meta("group_id")))
elif item.has_meta("objekte_kopf"):
_apply_membership_drop(indices, -1)
## Mitgliedschafts-Kern (testbar): gid ≥ 0 = aufnehmen, gid < 0 = herauslösen.
func _apply_membership_drop(indices: Array, gid: int) -> void:
var before := _snapshot_group_blocks()
var changed := 0
var label := ""
if gid >= 0:
changed = GroupState.assign_group(main.doc, indices, gid)
label = "%d Objekt(e) in „%s“ gezogen" % [changed, GroupState.display_name(main.doc, gid)]
else:
changed = GroupState.clear_group_fields(main.doc, indices)
label = "%d Objekt(e) aus der Gruppe herausgelöst" % changed
if changed == 0:
main.call("_set_status", "Nichts zu ändern — die Objekte gehören schon genau so zur Gruppe.")
return
GroupState.prune_orphans(main.doc)
main.history.commit(label, before, _snapshot_group_blocks())
main.call("_refresh_history_buttons")
_refresh_after_group_change()
_select_for_action(indices) # gezogene Objekte bleiben sichtbar markiert
## Nach jeder Gruppen-Änderung: Baum neu + Panels synchron (kein 3D-Rebuild —
## Positionen ändern sich beim Gruppieren/Umbenennen/Zuordnen nicht).
func _refresh_after_group_change() -> void:
main.call("_refresh_tree")
var sel: Array = main.interaction.selected_indices
_tree_select_lock = true
mark_tree_selection_multi(sel)
_tree_select_lock = false
main.call("_sync_selection_panel")