342 lines
16 KiB
GDScript
342 lines
16 KiB
GDScript
extends SceneTree
|
|
## test_scene_groups (Karte t_a4bf9b8c, U3 Gruppen im Szenen-Baum): prüft
|
|
## Gruppieren/Auflösen/Umbenennen per Baum, Drag-Mitgliedschaft, Gruppenbewegung,
|
|
## Ein-Undo-Schritt je Aktion, Alte-Map-Kompatibilität und ID-Eindeutigkeit.
|
|
##
|
|
## Geprüft wird:
|
|
## T1 Mehrfachauswahl → „Gruppieren“ (Baum-Weg): Doc-Feld, EIN Undo-Schritt.
|
|
## T2 Umbenennen: Name im Doc (additiv), Baum-Zeile zeigt ihn, EIN Undo-Schritt
|
|
## (Undo entfernt den Namen auch von NIE-GESPEICHERTEM Zustand, Redo holt ihn).
|
|
## T3 Drag IN Gruppe: Mitgliedschaft ändert sich, EIN Undo-Schritt, can_drop
|
|
## nur an erlaubten Zielen.
|
|
## T4 Drag OUT (auf „Objekte“): nur die gezogenen Indizes lösen sich.
|
|
## T5 Gruppenbewegung: alle Mitglieder gleichmäßig (Abstände erhalten), EIN Undo.
|
|
## T6 Gruppe auflösen + Gruppe löschen (Rückfrage-Pfad) — je EIN Undo-Schritt,
|
|
## Undo holt gelöschte Objekte samt Gruppenbindung zurück.
|
|
## T7 Alte Karte ohne Gruppenzustand: Roundtrip unverändert (kein group-Feld,
|
|
## kein group_names-Block); Map MIT Gruppen: Roundtrip erhält alles.
|
|
## T8 Gruppen-IDs bleiben eindeutig.
|
|
|
|
var _fails := 0
|
|
var _oks := 0
|
|
var main
|
|
var tree_mod
|
|
|
|
|
|
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: Gruppen im Szenen-Baum (U3)")
|
|
var scene: PackedScene = load("res://core/scenes/main.tscn")
|
|
main = scene.instantiate()
|
|
root.add_child(main)
|
|
await process_frame
|
|
await process_frame
|
|
await process_frame
|
|
tree_mod = main.get("tree_mod")
|
|
_check(tree_mod != null, "tree_mod aus der Registry geladen")
|
|
_check(tree_mod.get("content_tree") != null, "content_tree im Modul vorhanden")
|
|
_check(tree_mod.get("_context_menu") != null, "Kontextmenü im Baum angelegt (U3)")
|
|
|
|
# Drei Objekte ins Doc (gleiche Basis wie test_scene_tree).
|
|
(main.doc["objects"] as Array).append({
|
|
"id": "barrel_001", "pos": [4.0, 0.0, 4.0], "scale": 1.0, "rot_y_deg": 0.0})
|
|
(main.doc["objects"] as Array).append({
|
|
"id": "box_001", "pos": [-6.0, 0.0, 2.0], "scale": 2.0, "rot_y_deg": 10.0})
|
|
(main.doc["objects"] as Array).append({
|
|
"id": "fremd_objekt_xyz", "pos": [0.0, 0.0, 8.0], "scale": 1.0, "rot_y_deg": 0.0})
|
|
main.call("_after_structure_change")
|
|
await process_frame
|
|
|
|
await _t1_gruppieren()
|
|
await _t2_umbenennen()
|
|
await _t3_drag_rein()
|
|
await _t4_drag_raus()
|
|
await _t5_gruppenbewegung()
|
|
await _t6_loesen_und_loeschen()
|
|
await _t7_alte_karte_roundtrip()
|
|
await _t8_ids_eindeutig()
|
|
_finish()
|
|
|
|
|
|
func _history_count() -> int:
|
|
return (main.history.get("entries") as Array).size()
|
|
|
|
|
|
## History-Index (Position des zuletzt angewandten Eintrags). Nach einem Undo
|
|
## ersetzt der nächste Commit den verworfenen Redo-Zweig — der Eintrags-Zähler
|
|
## wächst dann NICHT, aber der Index springt +1. „EIN Undo-Schritt“ prüfen wir
|
|
## deshalb über das Index-Delta (präzise über beide Fälle).
|
|
func _history_index() -> int:
|
|
return int(main.history.get("index"))
|
|
|
|
|
|
func _find_child(item: TreeItem, prefix: String) -> TreeItem:
|
|
for child in item.get_children():
|
|
if str(child.get_text(0)).begins_with(prefix):
|
|
return child
|
|
return null
|
|
|
|
|
|
func _group_node(gid: int) -> TreeItem:
|
|
var tree: Tree = tree_mod.get("content_tree")
|
|
var grp_head := _find_child(tree.get_root(), "Gruppen (")
|
|
if grp_head == null:
|
|
return null
|
|
for child in grp_head.get_children():
|
|
if child.has_meta("group_id") and int(child.get_meta("group_id")) == gid:
|
|
return child
|
|
return null
|
|
|
|
|
|
func _t1_gruppieren() -> void:
|
|
print("== T1 Gruppieren (Mehrfachauswahl → Baum) ==")
|
|
var before_steps := _history_count()
|
|
main.interaction.call("select_many", [0, 2])
|
|
tree_mod.call("group_selected")
|
|
await process_frame
|
|
var ids: Array = GroupOps.active_group_ids(main.doc)
|
|
_check(ids.size() == 1 and int(ids[0]) == 0, "T1 Gruppe 0 im Doc angelegt (ids=%s)" % str(ids))
|
|
_check(_history_count() == before_steps + 1,
|
|
"T1 GENAU EIN Undo-Schritt (%d → %d)" % [before_steps, _history_count()])
|
|
var node := _group_node(0)
|
|
_check(node != null and str(node.get_text(0)).contains("(2 Objekte)"),
|
|
"T1 Baum zeigt Gruppen-Knoten mit 2 Mitgliedern")
|
|
# Ein Objekt allein kann nicht gruppieren (Klartext-Meldung, kein Crash).
|
|
var steps_now := _history_count()
|
|
main.interaction.call("clear_selection", true)
|
|
main.interaction.call("select_object", 0, true)
|
|
tree_mod.call("group_selected")
|
|
_check(_history_count() == steps_now, "T1 Gruppieren mit < 2 Objekten = kein Undo-Schritt")
|
|
|
|
|
|
func _t2_umbenennen() -> void:
|
|
print("== T2 Umbenennen ==")
|
|
var before_steps := _history_count()
|
|
var changed: bool = tree_mod.call("rename_group", 0, "Lager")
|
|
_check(changed, "T2 rename_group meldet Änderung")
|
|
_check(GroupState.group_name(main.doc, 0) == "Lager", "T2 Name im Dokument (group_names-Block)")
|
|
var node := _group_node(0)
|
|
_check(node != null and str(node.get_text(0)).begins_with("Lager "),
|
|
"T2 Baum-Zeile zeigt den Namen („%s“)" % (str(node.get_text(0)) if node != null else "?"))
|
|
_check(_history_count() == before_steps + 1,
|
|
"T2 GENAU EIN Undo-Schritt (%d → %d)" % [before_steps, _history_count()])
|
|
# Undo muss den NIE-GESPEICHERTEN Namen-Block entfernen (null-Seed).
|
|
main.call("_do_undo")
|
|
await process_frame
|
|
_check(not main.doc.has("group_names"), "T2 Undo entfernt group_names-Block komplett (alte-Map-Semantik)")
|
|
_check(GroupOps.active_group_ids(main.doc).size() == 1, "T2 Undo lässt die GRUPPE selbst bestehen")
|
|
main.call("_do_redo")
|
|
await process_frame
|
|
_check(GroupState.group_name(main.doc, 0) == "Lager", "T2 Redo stellt den Namen wieder her")
|
|
# Umbenennen auf denselben Namen = kein neuer Undo-Eintrag.
|
|
var steps_now := _history_count()
|
|
tree_mod.call("rename_group", 0, "Lager")
|
|
_check(_history_count() == steps_now, "T2 Rename auf denselben Namen = kein Undo-Schritt")
|
|
|
|
|
|
func _t3_drag_rein() -> void:
|
|
print("== T3 Drag in Gruppe ==")
|
|
var data: Dictionary = {"svmap_tree_objekte": [1]}
|
|
# can_drop: Gruppen-Knoten ja, Objekte-Kopf mit ungruppiertem Objekt nein.
|
|
var g_node := _group_node(0)
|
|
_check(g_node != null and tree_mod.call("_tree_can_drop", Vector2.ZERO, data) == false,
|
|
"T3 can_drop braucht die ECHTE Position (Vector2.ZERO = keine Zeile) — kein Blind-True")
|
|
_check(tree_mod.call("_tree_can_drop", Vector2(0, 0), {"falsch": 1}) == false,
|
|
"T3 fremdes Datenpaket wird abgelehnt")
|
|
var before_steps := _history_count()
|
|
tree_mod.call("_apply_membership_drop", [1], 0)
|
|
await process_frame
|
|
var members: Array = GroupOps.members_of(main.doc, 0)
|
|
_check(members == ([0, 1, 2] as Array), "T3 Objekt 1 ist jetzt Mitglied (members=%s)" % str(members))
|
|
_check(_history_count() == before_steps + 1,
|
|
"T3 GENAU EIN Undo-Schritt (%d → %d)" % [before_steps, _history_count()])
|
|
var node := _group_node(0)
|
|
_check(node != null and str(node.get_text(0)).contains("(3 Objekte)"),
|
|
"T3 Baum zeigt 3 Mitglieder")
|
|
# Drag-Start (Zeile finden + Menge): Ziehmenge = ganze Markierung, wenn die
|
|
# Zeile Teil der Auswahl ist — sonst nur die Zeile.
|
|
main.interaction.call("select_many", [1, 2])
|
|
var drag_item: TreeItem = null
|
|
for child in node.get_children():
|
|
if child.has_meta("obj_index") and int(child.get_meta("obj_index")) == 1:
|
|
drag_item = child
|
|
_check(drag_item != null, "T3 gezogene Zeile (Objekt 1) unter dem Gruppen-Knoten gefunden")
|
|
if drag_item != null:
|
|
var picked: Array = tree_mod.call("_collect_drag_indices", drag_item)
|
|
_check(picked == ([1, 2] as Array), "T3 Ziehmenge = ganze Markierung [1, 2] (hat %s)" % str(picked))
|
|
|
|
|
|
func _t4_drag_raus() -> void:
|
|
print("== T4 Drag aus Gruppe (auf „Objekte“) ==")
|
|
# can_drop auf Objekte-Kopf: nur erlaubt, wenn EIN gezogenes Objekt gruppiert ist.
|
|
var objects: Array = main.doc.get("objects", [])
|
|
_check(GroupOps.group_id(objects[0]) == 0, "T4 Vorbedingung: Objekt 0 ist in Gruppe 0")
|
|
var before_steps := _history_count()
|
|
tree_mod.call("_apply_membership_drop", [0], -1)
|
|
await process_frame
|
|
_check(GroupOps.group_id(objects[0]) == GroupOps.UNGROUPED, "T4 Objekt 0 ist ungruppiert")
|
|
_check(GroupOps.members_of(main.doc, 0) == ([1, 2] as Array), "T4 Gruppe 0 bleibt mit 1+2 bestehen")
|
|
_check(_history_count() == before_steps + 1,
|
|
"T4 GENAU EIN Undo-Schritt (%d → %d)" % [before_steps, _history_count()])
|
|
# Undo holt Objekt 0 in die Gruppe zurück.
|
|
main.call("_do_undo")
|
|
await process_frame
|
|
_check(GroupOps.members_of(main.doc, 0) == ([0, 1, 2] as Array), "T4 Undo stellt die Mitgliedschaft wieder her")
|
|
|
|
|
|
func _t5_gruppenbewegung() -> void:
|
|
print("== T5 Gruppenbewegung (Abstände erhalten, EIN Undo) ==")
|
|
var members: Array = GroupOps.members_of(main.doc, 0)
|
|
var objects: Array = main.doc.get("objects", [])
|
|
var pos_before: Dictionary = {}
|
|
for i in members:
|
|
pos_before[int(i)] = Vector3(
|
|
float((objects[i] as Dictionary)["pos"][0]),
|
|
float((objects[i] as Dictionary)["pos"][1]),
|
|
float((objects[i] as Dictionary)["pos"][2]))
|
|
var dist_before: float = pos_before[1].distance_to(pos_before[2])
|
|
main.interaction.call("select_many", members)
|
|
var before_steps := _history_count()
|
|
var before_index := _history_index()
|
|
main.interaction.call("nudge_selection", Vector3(4.0, 0.0, -2.0))
|
|
await process_frame
|
|
var moved: Dictionary = {}
|
|
for i in members:
|
|
var p: Array = (main.doc["objects"][int(i)] as Dictionary)["pos"]
|
|
moved[int(i)] = Vector3(float(p[0]), float(p[1]), float(p[2]))
|
|
var delta: Vector3 = moved[1] - pos_before[1]
|
|
var dist_after: float = moved[1].distance_to(moved[2])
|
|
_check(delta.length() > 0.5, "T5 Mitglieder sind tatsächlich gewandert (%.2f m)" % delta.length())
|
|
_check(absf((moved[2] - pos_before[2]).length() - delta.length()) < 0.001,
|
|
"T5 ALLE Mitglieder um denselben Betrag verschoben")
|
|
_check(absf(dist_after - dist_before) < 0.001,
|
|
"T5 Abstand erhalten (%.3f m → %.3f m)" % [dist_before, dist_after])
|
|
_check(_history_index() == before_index + 1,
|
|
"T5 GENAU EIN Undo-Schritt (Index %d → %d, Einträge %d)" % [before_index, _history_index(), _history_count()])
|
|
main.call("_do_undo")
|
|
await process_frame
|
|
var objects2: Array = main.doc.get("objects", [])
|
|
var restored: bool = true
|
|
for i in members:
|
|
var p: Array = (objects2[int(i)] as Dictionary)["pos"]
|
|
if Vector3(float(p[0]), float(p[1]), float(p[2])).distance_to(pos_before[int(i)]) > 0.001:
|
|
restored = false
|
|
_check(restored, "T5 EIN Undo stellt alle Positionen exakt wieder her")
|
|
|
|
|
|
func _t6_loesen_und_loeschen() -> void:
|
|
print("== T6 Auflösen + Löschen (Rückfrage-Pfad) ==")
|
|
var before_steps := _history_count()
|
|
var before_index := _history_index()
|
|
tree_mod.call("ungroup_group", 0)
|
|
await process_frame
|
|
_check(GroupOps.active_group_ids(main.doc).is_empty(), "T6 Gruppe 0 komplett aufgelöst")
|
|
var field_free := true
|
|
for obj in (main.doc["objects"] as Array):
|
|
if (obj as Dictionary).has("group"):
|
|
field_free = false
|
|
_check(field_free, "T6 kein Objekt trägt noch ein group-Feld")
|
|
_check(_history_index() == before_index + 1,
|
|
"T6 Auflösen = GENAU EIN Undo-Schritt (Index %d → %d, Einträge %d)" % [before_index, _history_index(), _history_count()])
|
|
# Neu gruppieren, dann löschen (Rückfrage bei > 1 Objekt → Dialog; Kern direkt).
|
|
main.interaction.call("select_many", [0, 1])
|
|
tree_mod.call("group_selected")
|
|
await process_frame
|
|
var ids: Array = GroupOps.active_group_ids(main.doc)
|
|
_check(ids.size() == 1, "T6 Gruppe neu angelegt (ids=%s)" % str(ids))
|
|
var gid := int(ids[0])
|
|
var before_del := _history_count()
|
|
tree_mod.call("delete_group_confirmed", gid)
|
|
await process_frame
|
|
_check((main.doc["objects"] as Array).size() == 1, "T6 beide Gruppen-Mitglieder gelöscht (1 Objekt übrig)")
|
|
_check(_history_count() == before_del + 1,
|
|
"T6 Löschen = GENAU EIN Undo-Schritt (%d → %d)" % [before_del, _history_count()])
|
|
main.call("_do_undo")
|
|
await process_frame
|
|
_check((main.doc["objects"] as Array).size() == 3, "T6 EIN Undo holt beide Objekte zurück")
|
|
var back_ids: Array = GroupOps.active_group_ids(main.doc)
|
|
_check(back_ids.size() == 1 and GroupOps.members_of(main.doc, int(back_ids[0])).size() == 2,
|
|
"T6 Undo stellt auch die Gruppen-Bindung wieder her")
|
|
# Aufräumen für T7/T8: Gruppe lösen, alle Objekte einzeln.
|
|
tree_mod.call("ungroup_group", int(back_ids[0]))
|
|
|
|
|
|
func _t7_alte_karte_roundtrip() -> void:
|
|
print("== T7 Alte Karte ohne Gruppenzustand lädt unverändert ==")
|
|
var doc := SvmapDocument.create_new("U3-Alte-Karte", 256.0, "mittag")
|
|
var save1 := SvmapIO.save_to_file("user://test_scene_groups_alt.svmap", doc)
|
|
_check(save1["ok"], "T7 alte Karte speichert ok: %s" % str(save1["errors"]))
|
|
var load1 := SvmapIO.load_from_file("user://test_scene_groups_alt.svmap")
|
|
_check(load1["ok"], "T7 alte Karte lädt ok: %s" % str(load1["errors"]))
|
|
var loaded: Dictionary = load1["data"]
|
|
_check(not loaded.has("group_names"), "T7 geladene Map hat KEINEN group_names-Block")
|
|
var no_group_field := true
|
|
for obj in (loaded["objects"] as Array):
|
|
if (obj as Dictionary).has("group"):
|
|
no_group_field = false
|
|
_check(no_group_field, "T7 kein Objekt trägt ein group-Feld")
|
|
# Anzeige-Fallback ohne Namen: "Gruppe 1" statt Crash.
|
|
_check(GroupState.display_name(loaded, 0) == "Gruppe 1", "T7 Anzeige-Fallback „Gruppe 1“ ohne Namen")
|
|
# Map MIT Gruppen: Roundtrip erhält group-Feld UND Namen.
|
|
var doc2: Dictionary = SvmapDocument.create_new("U3-Mit-Gruppen", 256.0, "mittag")
|
|
(doc2["objects"] as Array).append({
|
|
"id": "barrel_001", "pos": [4.0, 0.0, 4.0], "scale": 1.0, "rot_y_deg": 0.0})
|
|
(doc2["objects"] as Array).append({
|
|
"id": "box_001", "pos": [-6.0, 0.0, 2.0], "scale": 2.0, "rot_y_deg": 10.0})
|
|
GroupOps.group(doc2, [0, 1])
|
|
GroupState.set_group_name(doc2, 0, "Lager")
|
|
var save2 := SvmapIO.save_to_file("user://test_scene_groups_neu.svmap", doc2)
|
|
_check(save2["ok"], "T7 Map mit Gruppen speichert ok: %s" % str(save2["errors"]))
|
|
var load2 := SvmapIO.load_from_file("user://test_scene_groups_neu.svmap")
|
|
_check(load2["ok"], "T7 Map mit Gruppen lädt ok: %s" % str(load2["errors"]))
|
|
var loaded2: Dictionary = load2["data"]
|
|
_check(GroupState.group_name(loaded2, 0) == "Lager", "T7 Gruppenname überlebt Roundtrip")
|
|
_check(GroupOps.group_id(loaded2["objects"][0]) == 0
|
|
and GroupOps.group_id(loaded2["objects"][1]) == 0,
|
|
"T7 group-Felder überleben Roundtrip")
|
|
|
|
|
|
func _t8_ids_eindeutig() -> void:
|
|
print("== T8 Gruppen-IDs bleiben eindeutig ==")
|
|
var objects: Array = main.doc.get("objects", [])
|
|
_check(objects.size() == 3, "T8 Vorbedingung: 3 Objekte im Doc")
|
|
var seen: Dictionary = {}
|
|
# Mehrere Gruppen nacheinander anlegen — jede ID muss neu sein.
|
|
var g1: int = GroupOps.group(main.doc, [0, 1])
|
|
seen[g1] = true
|
|
_check(g1 >= 0, "T8 Gruppe 1 angelegt (id %d)" % g1)
|
|
var g2: int = GroupOps.group(main.doc, [2])
|
|
_check(g2 < 0, "T8 Gruppe braucht ≥ 2 Objekte (id %d)" % g2)
|
|
# Objekt 2 ist nicht gruppiert → mit Objekt 0 gruppieren → verlässt Gruppe 1.
|
|
var g2b: int = GroupOps.group(main.doc, [2, 0])
|
|
_check(g2b != g1, "T8 zweite Gruppe bekommt eine NEUE ID (%d ≠ %d)" % [g2b, g1])
|
|
seen[g2b] = true
|
|
var active: Array = GroupOps.active_group_ids(main.doc)
|
|
var unique_ids: Dictionary = {}
|
|
for g in active:
|
|
unique_ids[int(g)] = true
|
|
_check((active as Array).size() == unique_ids.size(),
|
|
"T8 aktive IDs sind eindeutig (ids=%s)" % str(active))
|
|
_check(GroupOps.next_group_id(main.doc) > int(active.max()), "T8 nächste ID liegt über allen aktiven")
|
|
# Aufräumen: alles ungruppiert für den restlichen Lauf.
|
|
GroupState.clear_group_fields(main.doc, [0, 1, 2])
|
|
GroupState.prune_orphans(main.doc)
|
|
_check(GroupOps.active_group_ids(main.doc).is_empty(), "T8 Aufräumen: keine Gruppen mehr aktiv")
|
|
|
|
|
|
func _finish() -> void:
|
|
print("CHECKS: %d FAILURES: %d" % [_oks + _fails, _fails])
|
|
if _fails > 0:
|
|
print("SCENE_GROUPS_TEST_FAIL")
|
|
quit(1)
|
|
else:
|
|
print("ALL OK: %d checks" % (_oks + _fails))
|
|
quit(0)
|