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