extends SceneTree ## Editor-Test (headless): Einmal-Platzieren — Karte t_5e3e97c9 (U5). ## Ausführen: godot4 --headless --path . --script res://tests/test_place_once.gd ## Exit 0 = alles grün. ## ## Verhalten (Spec KARTEN_UI_UNITY_vc27, Karte U5): ## 1. EIN Klick setzt EIN Objekt → Platzier-Werkzeug AUS, Ghost/Panel- ## Auswahl zurück, das NEUE Objekt ist ausgewählt (Index korrekt, ## Baum + Inspektor aktualisiert), Statuszeile „Objekt gesetzt…“. ## 2. Shift beim Klick = Serienmodus: Werkzeug bleibt AN, weitere Klicks ## setzen mehrfach; die Statuszeile zeigt „Serienmodus (Shift)“. ## 3. ESC / Rechtsklick bricht ab OHNE zu setzen. ## 4. EIN Undo entfernt genau das eine gesetzte Objekt. ## 5. Auswahl bleibt stabil: kein zweites Auswahl-Ereignis für dasselbe ## Objekt (Baum-Markierung nur 1×). ## ## Instanziert die ECHTE Main-UI (Muster tests/test_rmb_vorrang.gd) und ## schickt echte Maus-/Tastatur-Events durch den SubViewport; die direkten ## interaction-Pfade laufen gegen ein Probe-Main wie tests/test_shortcuts.gd. var failures := 0 var checks := 0 var main: Control var subvp: SubViewport var selected_count := 0 # Zähler für die Auswahl-Stabilität (Baum-Markierungen). var tree_marks := 0 func _initialize() -> void: _run_all.call_deferred() func _run_all() -> void: _clear_library_cfg_section() var scene: PackedScene = load("res://scenes/main.tscn") _check(scene != null, "scenes/main.tscn laedt") if scene == null: _finish() return main = scene.instantiate() root.add_child(main) await process_frame await process_frame var container := _find_viewport_container(main) _check(container != null, "SubViewportContainer gefunden") if container == null: _finish() return subvp = container.get_child(0) as SubViewport _check(subvp != null, "SubViewport gefunden") await _test_ein_klick_setzt_einmal() await _test_serienmodus_shift() await _test_esc_bricht_ab() await _test_rechtsklick_laesst_nicht_fallen() _test_undo_genau_eins() _test_probe_auswahl_stabil() _finish() func _finish() -> void: if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1) func _check(cond: bool, label: String) -> void: checks += 1 if cond: print(" ok: %s" % label) else: failures += 1 print(" FAIL: %s" % label) func _find_viewport_container(node: Node) -> SubViewportContainer: if node is SubViewportContainer: return node as SubViewportContainer for child in node.get_children(): var found := _find_viewport_container(child) if found != null: return found return null ## Event-Position: Mitte des 3D-Viewports (SubViewport-Koordinaten) — trifft ## garantiert die Karte (K1d-Startansicht: 80 m über der Kartenmitte). func _vp_center() -> Vector2: return subvp.size * 0.5 func _mouse(button: MouseButton, pressed: bool, pos: Vector2, shift := false) -> void: var mb := InputEventMouseButton.new() mb.button_index = button mb.pressed = pressed mb.position = pos mb.global_position = pos mb.shift_pressed = shift subvp.push_input(mb) func _motion(pos: Vector2, shift := false) -> void: var mm := InputEventMouseMotion.new() mm.position = pos mm.global_position = pos mm.relative = Vector2.ZERO mm.shift_pressed = shift subvp.push_input(mm) func _press_key(keycode: Key) -> InputEventKey: var ev := InputEventKey.new() ev.keycode = keycode ev.physical_keycode = keycode ev.pressed = true return ev func _objects() -> Array: return main.doc.get("objects", []) as Array func _first_oid() -> String: var lib: Dictionary = main.manifest.get("objects", {}) for key in lib: return str(key) return "" # ------------------------------------------------------------ 1 Ein Klick ---- func _test_ein_klick_setzt_einmal() -> void: print("Testgruppe: Ein Klick setzt EIN Objekt und beendet das Werkzeug") var oid := _first_oid() _check(oid != "", "Manifest hat ein Objekt zum Platzieren") if oid == "": return var interaction: Node = main.interaction var assets: Node = main.assets_mod selected_count = 0 assets.object_selected.connect(func(_s: String) -> void: selected_count += 1) main.call("_set_tool_mode", "objects") _check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size()) # Doppelklick auf die Kachel = Werkzeug-Start (U4-Vertrag). assets._activate_tile(oid) _check(bool(interaction.get("placing")), "Werkzeug läuft (Objekt in der Hand)") _check(str(interaction.get("place_oid")) == oid, "place_oid stimmt (%s)" % str(interaction.get("place_oid"))) var placed_index := _objects().size() _check(placed_index == 0, "nichts gesetzt vor dem Klick (Index des Neuen = %d)" % placed_index) # EIN echter Linksklick in die Kartenmitte (ohne Shift). var pos := _vp_center() _motion(pos) _mouse(MOUSE_BUTTON_LEFT, true, pos) await process_frame _mouse(MOUSE_BUTTON_LEFT, false, pos) await process_frame _check(_objects().size() == 1, "EIN Klick = EIN Objekt im doc (ist %d)" % _objects().size()) _check(not bool(interaction.get("placing")), "Werkzeug nach dem Setzen AUS") _check(str(interaction.get("place_oid")) == "", "Ghost-Auftrag geleert (place_oid leer)") _check(int(interaction.get("selected_index")) == placed_index, "das neue Objekt ist ausgewählt (Index %d, ist %d)" % [placed_index, int(interaction.get("selected_index"))]) var indices_arr: Variant = interaction.get("selected_indices") var indices: Array = indices_arr if indices_arr is Array else [] _check(indices == ([placed_index] as Array), "Auswahl-Liste = [neues Objekt] (ist %s)" % str(indices)) _check(str(assets.get("selected_oid")) == "", "Panel-Auswahl zurückgesetzt (Kachel leuchtet nicht mehr)") _check(str(main.status_label.text).contains("ausgewählt"), "Statuszeile meldet Klartext („…ausgewählt“): %s" % str(main.status_label.text)) # Inspektor zeigt das neue Objekt. var inspector: Node = main.get("inspector") _check(inspector != null, "Inspektor vorhanden") if inspector != null: var head_txt: String = str(inspector.get("_head").text) _check(not str(head_txt).is_empty() and head_txt != "Objekt-Inspektor", "Inspektor zeigt das neue Objekt (%s)" % head_txt) # Zweiter Klick NACH dem Ende: wählt nichts / setzt nichts (Werkzeug ist aus). var before := _objects().size() _mouse(MOUSE_BUTTON_LEFT, true, pos) await process_frame _mouse(MOUSE_BUTTON_LEFT, false, pos) await process_frame _check(_objects().size() == before, "Klick danach setzt NICHTS (Werkzeug beendet)") _check(selected_count == 1, "Auswahl bleibt stabil: object_selected genau 1× (ist %d)" % selected_count) await _cleanup_objects() # ----------------------------------------------------------- 2 Serienmodus ---- func _test_serienmodus_shift() -> void: print("Testgruppe: Shift = Serienmodus — Werkzeug bleibt AN, setzt mehrfach") var oid := _first_oid() var interaction: Node = main.interaction var assets: Node = main.assets_mod main.call("_set_tool_mode", "objects") _check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size()) assets._activate_tile(oid) _check(bool(interaction.get("placing")), "Werkzeug läuft") var pos0 := _vp_center() # Klickpunkte leicht streuen: der vp-Mittelpunkt liegt exakt auf der # Symmetrie-Mitte (Standard Punktspiegelung) — dort wird absichtlich NICHT # gedoppelt, der nächste Klick liegt daneben und zeigt den Zwilling. # Für U5 unerheblich (beide Varianten setzen), aber die Zählung bleibt # eindeutig, wenn jeder Klick off-center landet. var pos := Vector2(pos0.x + 17.0, pos0.y + 11.0) # Klick MIT Shift: Serienmodus — Werkzeug bleibt an, setzt trotzdem. _motion(pos, true) _mouse(MOUSE_BUTTON_LEFT, true, pos, true) await process_frame _mouse(MOUSE_BUTTON_LEFT, false, pos, true) await process_frame _check(_objects().size() == 1, "1. Klick (Shift) setzt genau eins (ist %d)" % _objects().size()) _check(bool(interaction.get("placing")), "SERIENMODUS: Werkzeug bleibt AN") _check(str(main.status_label.text).contains("Serienmodus"), "Statuszeile nennt den Serienmodus: %s" % str(main.status_label.text)) _check(int(interaction.get("selected_index")) == 0, "Serienmodus: das neue Objekt ist ausgewählt (Index 0)") # Weiterer Klick MIT Shift: setzt das zweite Objekt, Werkzeug bleibt an. pos = Vector2(pos0.x - 23.0, pos0.y + 7.0) _motion(pos, true) _mouse(MOUSE_BUTTON_LEFT, true, pos, true) await process_frame _mouse(MOUSE_BUTTON_LEFT, false, pos, true) await process_frame _check(_objects().size() >= 2 and _objects().size() % 2 == 0, "1./2. Klick (Shift) setzen (mit Zwillingen: gerade Zahl — ist %d)" % _objects().size()) _check(bool(interaction.get("placing")), "Serienmodus läuft weiter (Werkzeug noch an)") # Klick OHNE Shift in der Serie: beendet das Werkzeug (normales Ende). pos = Vector2(pos0.x + 9.0, pos0.y - 13.0) _motion(pos) _mouse(MOUSE_BUTTON_LEFT, true, pos) await process_frame _mouse(MOUSE_BUTTON_LEFT, false, pos) await process_frame _check(_objects().size() == 3, "3. Klick (ohne Shift) setzt das dritte (ist %d)" % _objects().size()) _check(not bool(interaction.get("placing")), "ohne Shift: Werkzeug beendet sich") await _cleanup_objects() # ------------------------------------------------------------- 3 ESC bricht ---- func _test_esc_bricht_ab() -> void: print("Testgruppe: ESC bricht ab — nichts wird gesetzt") var oid := _first_oid() var interaction: Node = main.interaction var assets: Node = main.assets_mod main.call("_set_tool_mode", "objects") _check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size()) assets._activate_tile(oid) _check(bool(interaction.get("placing")), "Werkzeug läuft") var pos := _vp_center() _motion(pos) main._unhandled_key_input(_press_key(KEY_ESCAPE)) await process_frame _check(not bool(interaction.get("placing")), "ESC beendet das Werkzeug") _check(_objects().size() == 0, "ESC hat NICHTS gesetzt (ist %d)" % _objects().size()) _check(str(interaction.get("place_oid")) == "", "Ghost-Auftrag weg") _check(str(main.status_label.text).contains("abgebrochen") or str(main.status_label.text).contains("Bereit"), "Statuszeile meldet den Abbruch (%s)" % str(main.status_label.text)) await _cleanup_objects() # ------------------------------------------------------- 4 Rechtsklick = Flug ---- func _test_rechtsklick_laesst_nicht_fallen() -> void: print("Testgruppe: Rechtsklick beim Platzieren = Flug (Objekt bleibt, RMB setzt NICHT)") var oid := _first_oid() var interaction: Node = main.interaction var assets: Node = main.assets_mod var camera: Camera3D = main.get("camera") main.call("_set_tool_mode", "objects") assets._activate_tile(oid) _check(bool(interaction.get("placing")), "Werkzeug läuft") var pos := _vp_center() _motion(pos) # Rechte Maustaste hat absoluten Vorrang (t_2470de89): Flug, kein Setzen, # kein Abbruch. _mouse(MOUSE_BUTTON_RIGHT, true, pos) await process_frame _check(bool(camera.call("fly_active")), "RMB → Flug an (Vorrang)") _check(bool(interaction.get("placing")), "Objekt bleibt in der Hand (kein Abbruch, kein Setzen)") _mouse(MOUSE_BUTTON_RIGHT, false, pos) await process_frame _check(not bool(camera.call("fly_active")), "RMB los → Flug aus") _check(bool(interaction.get("placing")), "Werkzeug läuft nach dem Flug weiter") # ESC wirft ab (bekannter Vertrag). main._unhandled_key_input(_press_key(KEY_ESCAPE)) await process_frame _check(not bool(interaction.get("placing")), "ESC wirft danach ab") _check(_objects().size() == 0, "kein Objekt gesetzt (ist %d)" % _objects().size()) await _cleanup_objects() # ------------------------------------------------------------- 5 EIN Undo ---- func _test_undo_genau_eins() -> void: print("Testgruppe: Ein Undo nimmt genau das eine gesetzte Objekt zurück") var oid := _first_oid() var interaction: Node = main.interaction var assets: Node = main.assets_mod main.call("_set_tool_mode", "objects") await _cleanup_objects() _check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size()) assets._activate_tile(oid) var pos := _vp_center() _motion(pos) _mouse(MOUSE_BUTTON_LEFT, true, pos) await process_frame _mouse(MOUSE_BUTTON_LEFT, false, pos) await process_frame _check(_objects().size() == 1, "Objekt gesetzt (ist %d)" % _objects().size()) _check(main.history.can_undo(), "History hat den Platzier-Schritt") _check(str(main.history.undo_desc()).contains("platzieren"), "Undo-Beschreibung nennt das Platzieren (%s)" % str(main.history.undo_desc())) main._do_undo() await process_frame _check(_objects().size() == 0, "EIN Undo: das eine Objekt ist weg (ist %d)" % _objects().size()) _check(not main.history.can_undo(), "History ist leer (kein weiterer Schritt)") _check(int(interaction.get("selected_index")) == -1, "Auswahl nach dem Undo geleert (Index -1)") await _cleanup_objects() # --------------------------------------------------- 6 Auswahl-Stabilität ---- ## Direkter Pfad (Probe-Main wie tests/test_shortcuts.gd): nach _place_object ## wird die Baum-Markierung für das neue Objekt GENAU EINMAL gesetzt — kein ## doppeltes Auswahl-Ereignis (1× in der Platzier-Meldung, 1× im Werkzeug-Ende). func _test_probe_auswahl_stabil() -> void: print("Testgruppe: Auswahl stabil — Baum-Markierung genau 1× je Platzierung") var doc := SvmapDocument.create_new("U5Test", 1024.0, "mittag") var fake := ProbeMain.new() fake.doc = doc fake.manifest = {"objects": {"box_001": {"poly": 12, "collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}} fake.sym_mode = SymmetryCore.Mode.OFF # Auswahl-Stabilität, nicht M3 fake.history.begin(doc) var vp := SubViewport.new() vp.size = Vector2i(1280, 720) root.add_child(vp) var cam := Camera3D.new() vp.add_child(cam) cam.current = true cam.look_at_from_position(Vector3(-95.0, 115.0, 135.0), Vector3.ZERO, Vector3.UP) var builder := ProbeBuilder.new() var interaction := Node3D.new() interaction.set_script(load("res://src/editor/interaction.gd")) vp.add_child(interaction) interaction.setup(fake, cam, builder) # Werkzeug an (wie start_placement, ohne GLB-Abhängigkeit). interaction.set("placing", true) interaction.set("place_oid", "box_001") var h := SvmapDocument.terrain_height_at(doc, 100.0, 40.0) interaction.call("_place_object", cam.unproject_position(Vector3(100.0, h, 40.0))) var objs: Array = doc.get("objects", []) _check(objs.size() == 1, "Probe: 1 Objekt gesetzt (ist %d)" % objs.size()) _check(not bool(interaction.get("placing")), "Probe: Werkzeug nach dem Setzen AUS") _check(int(interaction.get("selected_index")) == 0, "Probe: neues Objekt ausgewählt (Index 0)") _check(fake.tree_marks == 1, "Probe: Baum-Markierung GENAU 1× (ist %d)" % fake.tree_marks) _check(fake.statuses.size() > 0 and str(fake.statuses[fake.statuses.size() - 1]).contains("ausgewählt"), "Probe: letzte Meldung sagt „ausgewählt“ (%s)" % str(fake.statuses.back() if not fake.statuses.is_empty() else "")) # Serienmodus-Gegenprobe: mit Shift bleibt das Werkzeug an. var inter2 := Node3D.new() inter2.set_script(load("res://src/editor/interaction.gd")) vp.add_child(inter2) var doc2 := SvmapDocument.create_new("U5Test2", 1024.0, "mittag") var fake2 := ProbeMain.new() fake2.doc = doc2 fake2.manifest = {"objects": {"box_001": {"poly": 12, "collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}} fake2.sym_mode = SymmetryCore.Mode.OFF # Serienmodus-Zählung ohne Zwillinge fake2.history.begin(doc2) inter2.setup(fake2, cam, builder) inter2.set("placing", true) inter2.set("place_oid", "box_001") inter2.call("_place_object", cam.unproject_position(Vector3(100.0, h, 40.0)), true) _check(bool(inter2.get("placing")), "Probe Serienmodus: Werkzeug bleibt AN (Shift)") _check((doc2["objects"] as Array).size() == 1, "Probe Serienmodus: Objekt gesetzt") _check(str(fake2.statuses.back() if not fake2.statuses.is_empty() else "").contains("Serienmodus"), "Probe: Statuszeile nennt „Serienmodus (Shift)“") # Zweiter serieller Klick: zweites Objekt, immer noch AN. inter2.set("placing", true) inter2.set("place_oid", "box_001") inter2.call("_place_object", cam.unproject_position(Vector3(-100.0, h, -40.0)), true) _check((doc2["objects"] as Array).size() == 2, "Probe Serienmodus: 2 Objekte gesetzt") _check(bool(inter2.get("placing")), "Probe Serienmodus: immer noch AN") vp.queue_free() # -------------------------------------------------------------- Helfer ---- ## Test-Map leeren (Undo-History bleibt bewusst stehen — die nächste Gruppe ## baut ihren eigenen Zustand auf). Alle Objekte hart entfernen. func _cleanup_objects() -> void: # Symmetrie aus (Standard ist Punkt) — die Serien-/Einmal-Zählung soll # nicht von automatischen Zwillingen abhängen (M3, separate Karte). main.sym_mode = SymmetryCore.Mode.OFF var objects: Array = main.doc.get("objects", []) objects.clear() main.view_builder.call("rebuild", main.doc, main.manifest) var interaction: Node = main.interaction interaction.call("clear_selection", true) main.history.clear() await process_frame func _clear_library_cfg_section() -> void: # Nur UNSEREN Abschnitt leeren — [test]/[update]/[fenster] bleiben. var cfg := ConfigFile.new() cfg.load("user://scopedvr_editor.cfg") if cfg.has_section("library"): cfg.erase_section("library") cfg.save("user://scopedvr_editor.cfg") ## Probe-main-Doppel (wie tests/test_shortcuts.gd ProbeMain): zählt ## Baum-Markierungen mit, damit die Auswahl-Stabilität beweisbar ist. class ProbeMain: var doc: Dictionary = {} var manifest: Dictionary = {} var grid_snap := false var dirty := false var sym_mode: int = SymmetryCore.Mode.OFF # U5-Probe: Symmetrie bewusst aus var history: ActionHistory = ActionHistory.new() var statuses: Array = [] var tree_marks := 0 func object_label(_oid: String) -> String: return "Kiste" func _fmt_int(v: int) -> String: return str(v) func _set_status(text: String, _err := false) -> void: statuses.append(text) func _highlight_library(_oid: String) -> void: pass func _selection_status(_idx: int) -> void: pass func _sync_selection_panel() -> void: pass func _mark_tree_selection(_idx: int) -> void: tree_marks += 1 func _mark_tree_selection_multi(_indices: Array) -> void: pass func _after_structure_change() -> void: pass func _refresh_history_buttons() -> void: pass func _refresh_stats() -> void: pass func _refresh_file_label() -> void: pass class ProbeBuilder extends Node3D: var object_markers: Array = [] var last_selection: Array = [] func set_selected_multi(indices: Array) -> void: last_selection = indices.duplicate()