U5 t_5e3e97c9: Platzieren EINMAL — ein Klick setzt, Werkzeug beendet sich, neues Objekt ausgewählt (Baum+Inspektor), Klartext-Status; Shift = Serienmodus (weiter setzen, Status nennt ESC); Ghost/Panel-Kachel nach dem Setzen zurück; eine Platzierung = ein Undo-Schritt. Abbruch via ESC (ShortcutPolicy) — Rechtsklick bleibt Flug-Vorrang (t_2470de89, test_rmb_vorrang). Neue Suite tests/test_place_once.gd (52 checks) + Renderprobe.

This commit is contained in:
Hermes (scopedvr-dev) 2026-10-20 10:15:01 +00:00
parent 403895b743
commit f16b9c8fb6
5 changed files with 666 additions and 10 deletions

View file

@ -32,6 +32,10 @@ var view_builder # script-erweitert, Variant
var placing := false
var place_oid := ""
var place_rot_deg := 0.0
# U5 (t_5e3e97c9): Shift beim Setz-Klick = Serienmodus — weiter setzen wie
# bisher (Ghost bleibt an, jeder Klick setzt). Ohne Shift wird EIN Objekt
# gesetzt und das Werkzeug beendet sich selbst; das neue Objekt ist ausgewählt.
var _place_series := false
var _ghost: Node3D # Ghost-ROOT: Position = Objekt-Fußpunkt —
# identisch zur platzierten Marker-Position
# (Karte t_4092e3a0: Ghost == Platzierung).
@ -195,6 +199,7 @@ func start_placement(oid: String) -> void:
place_oid = oid
placing = true
place_rot_deg = 0.0
_place_series = false # U5: jeder frische Start startet im Einmal-Modus
_ghost_active = false
refresh_terrain_cache() # Cursor trifft das GELÄNDE — Cache muss frisch sein
_ensure_ghost()
@ -211,11 +216,30 @@ func cancel_placement() -> void:
_ghost.visible = false
placing = false
place_oid = ""
_place_series = false
_ghost_active = false
main.call("_highlight_library", "")
main.call("_base_tool_status")
## U5 (t_5e3e97c9): Ein Setz-Klick beendet das Platzier-Werkzeug und wählt
## GENAU das neue Objekt aus (Baum-Markierung + Inspektor folgen über
## select_object). Ghost aus, Kachel-Hervorhebung im Assets-Panel aus
## (_highlight_library("")), das Objekt-Werkzeug bleibt das aktive Werkzeug.
## from_tree=true: der Baum wird bereits von _apply_selection_view markiert —
## kein zweites Auswahl-Ereignis (Auswahl-Stabilität, Karten-Test).
## Aufruf VOR der Klartext-Statusmeldung: die letzte Meldung gewinnt.
func finish_placement_and_select(new_index: int) -> void:
if _ghost != null and is_instance_valid(_ghost):
_ghost.visible = false
placing = false
place_oid = ""
_place_series = false
_ghost_active = false
main.call("_highlight_library", "")
select_object(new_index, true)
func _ensure_ghost() -> void:
if _ghost != null and is_instance_valid(_ghost):
return
@ -285,7 +309,7 @@ func _ghost_update(viewport_pos: Vector2) -> void:
_ghost.visible = true
func _place_object(viewport_pos: Vector2) -> void:
func _place_object(viewport_pos: Vector2, mb_shift_held := false) -> void:
var p: Variant = _ground_point(viewport_pos)
if p == null:
main.call("_set_status", "Da war kein Boden unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true)
@ -339,15 +363,25 @@ func _place_object(viewport_pos: Vector2) -> void:
main.history.commit("%s platzieren" % main.object_label(place_oid),
before, main.history.snapshot(["objects"]))
main.call("_after_structure_change")
if on_axis:
main.call("_set_status", "%s platziert — liegt auf der Symmetrie-Mitte und wird NICHT gedoppelt. Tipp: anklicken zum Verschieben (Ziehen) und Drehen (R oder Mausrad)." % main.object_label(place_oid))
elif twin_index >= 0:
main.call("_set_status", "%s platziert — der Zwilling auf der anderen Seite ist automatisch mitgesetzt (Auswahl zeigt ihn schwächer markiert). Strg+Z nimmt BEIDE zurück." % main.object_label(place_oid))
# U5 (t_5e3e97c9): Shift gehalten = Serienmodus (weiter setzen wie bisher,
# Ghost bleibt an) — sonst beendet das Werkzeug sich selbst und DAS NEUE
# Objekt ist ausgewählt (Baum markiert, Inspektor gefüllt).
if mb_shift_held:
_place_series = true
select_object(new_index, true)
if twin_index >= 0:
main.call("_set_status", "Serienmodus (Shift) — ESC beendet. %s platziert, Zwilling jeweils mitgesetzt; Strg+Z nimmt BEIDE je Schritt zurück. Weitere Klicks setzen erneut." % main.object_label(place_oid))
else:
main.call("_set_status", "%s platziert. Tipp: anklicken zum Verschieben (Ziehen) und Drehen (R oder Mausrad) — Entf löscht es." % main.object_label(place_oid))
# Platzier-Modus bleibt aktiv (Mehrfach-Platzieren ohne erneutes Klicken),
# Auswahl auf das frische Objekt legen? Nein — Ghost bleibt Vorrang; ESC beendet.
main.call("_mark_tree_selection", new_index)
main.call("_set_status", "Serienmodus (Shift) — ESC beendet. Weitere Klicks setzen erneut; Strg+Z nimmt je Schritt zurück.")
else:
var placed_oid := place_oid # U5: finish leert place_oid — Label vorher sichern
finish_placement_and_select(new_index)
if twin_index >= 0:
main.call("_set_status", "%s gesetzt und ausgewählt — der Spiegel-Zwilling wurde mitgesetzt; Strg+Z nimmt BEIDE zurück. Ziehen verschiebt, R oder Mausrad dreht, Entf löscht." % main.object_label(placed_oid))
elif on_axis:
main.call("_set_status", "%s gesetzt und ausgewählt — liegt auf der Symmetrie-Mitte und wird NICHT gedoppelt. Ziehen verschiebt, R oder Mausrad dreht, Entf löscht." % main.object_label(placed_oid))
else:
main.call("_set_status", "%s gesetzt und ausgewählt. Ziehen verschiebt, R oder Mausrad dreht, Entf löscht, Strg+Z nimmt es zurück." % main.object_label(placed_oid))
# --------------------------------------------------------------- Auswahl ----
@ -1096,7 +1130,7 @@ func _unhandled_input(event: InputEvent) -> void:
func _on_left_press(mb: InputEventMouseButton) -> void:
if placing:
_place_object(mb.position)
_place_object(mb.position, mb.shift_pressed) # U5: Shift = Serienmodus
return
_press_pos = mb.position
var hit := _pick_object(mb.position)

View file

@ -0,0 +1,142 @@
extends SceneTree
## Beweis-Probe (Karte t_5e3e97c9, WEGWERF — nicht Teil der Suite):
## U5 Platzieren: Bildfolge VORHER (Ghost in der Hand) → GESETZT (Marker da,
## Werkzeug aus) → AUSGEWÄHLT (Auswahl-Markierung + Inspektor-Tab sichtbar).
## Der Platzier-Pfad ist der ECHTE: interaction._place_object (gleicher Pfad
## wie ein Linksklick, Karten-Regel: kein simuliertes Umfeld).
## Aufruf: xvfb-run -a godot4 --path . --rendering-driver opengl3 \
## -s res://tests/probe_u5_place_once_render.gd
func _initialize() -> void:
_run.call_deferred()
func _shot(pfad: String) -> void:
await process_frame
await RenderingServer.frame_post_draw
var img := root.get_viewport().get_texture().get_image()
img.save_png(pfad)
print("Bild: %s (%dx%d)" % [pfad, img.get_width(), img.get_height()])
## Rechts-Tabs: den Tab mit „Inspektor“ nach vorn holen (Fallback „Baum“),
## damit die Auswahl im Beweis-Bild sichtbar wird.
func _show_selection_tab(main: Node) -> String:
var stack: Array = [main]
while not stack.is_empty():
var n: Node = stack.pop_back()
if n is TabContainer:
var tabs := n as TabContainer
for i in tabs.get_tab_count():
var titel := tabs.get_tab_title(i)
if titel.contains("Auswahl") or titel.contains("Baum"):
tabs.current_tab = i
return titel
stack.append_array(n.get_children())
return ""
## „Modelle fehlen“-AcceptDialog schließen (kommt erst nach 2 Frames von
## utils/modelle_status.gd — mehrfach probieren, mit Wartezeit dazwischen).
func _dismiss_dialogs() -> void:
for i in 6:
await process_frame
var stack: Array = [root]
while not stack.is_empty():
var n: Node = stack.pop_back()
if n is AcceptDialog:
(n as AcceptDialog).hide()
n.queue_free()
stack.append_array(n.get_children())
func _run() -> void:
var scene: PackedScene = load("res://scenes/main.tscn")
var main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
# „Modelle fehlen“-Hinweis schließen (Beweis-Bilder sollen die Karte zeigen).
await _dismiss_dialogs()
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_size(Vector2i(2328, 1310))
await process_frame
DisplayServer.window_set_size(Vector2i(2328, 1310))
var interaction = main.get("interaction")
var oid := ""
var lib: Dictionary = main.manifest.get("objects", {})
for key in lib:
oid = str(key)
break
if oid == "":
print("PROBE_U5_FAIL: kein Manifest-Objekt")
quit(1)
return
main.call("_set_tool_mode", "objects")
interaction.call("start_placement", oid)
await process_frame
await process_frame
var cam = main.get("camera")
if cam != null:
cam.set("distance", 22.0)
cam.call("focus_on", Vector3(0.0, 0.0, 0.0))
await process_frame
await process_frame
# Cursor übers Karte-Zentrum → Ghost sichtbar parken (echter Ghost-Pfad).
var vp: SubViewport = null
var stack: Array = [main]
while not stack.is_empty():
var n: Node = stack.pop_back()
if n is SubViewportContainer:
vp = n.get_child(0) as SubViewport
break
stack.append_array(n.get_children())
if vp == null:
print("PROBE_U5_FAIL: kein SubViewport")
quit(1)
return
var mm := InputEventMouseMotion.new()
mm.position = (vp.size as Vector2) * 0.5
mm.global_position = mm.position
vp.push_input(mm)
await process_frame
await process_frame
var dir := "/tmp/u5_beweise"
DirAccess.make_dir_recursive_absolute(dir)
await _shot(dir + "/u5_1_vorher_ghost.png")
print("PRUEF vorher: placing=%s objects=%s" % [
str(bool(interaction.get("placing"))),
str((main.doc["objects"] as Array).size())])
# ECHTER Platzier-Pfad (wie Linksklick, ohne Shift → Einmal-Modus).
interaction.call("_place_object", mm.position)
await process_frame
await process_frame
await _shot(dir + "/u5_2_gesetzt.png")
var placed := (main.doc["objects"] as Array).size()
var builder = main.get("view_builder")
var sel_mat: StandardMaterial3D = builder.get("_select_mat")
var marker = null
if placed > 0:
marker = builder.object_markers[placed - 1].get_child(0)
var hat_auswahl: bool = marker != null and marker.material_override == sel_mat
var insp = main.get("inspector")
var head_txt := ""
if insp != null and insp.get("_head") != null:
head_txt = str(insp.get("_head").text)
print("PRUEF gesetzt: placing=%s objects=%s selected_index=%s marker_auswahl=%s inspektor='%s'" % [
str(bool(interaction.get("placing"))), str(placed),
str(interaction.get("selected_index")), str(hat_auswahl), head_txt])
print("PRUEF status='%s'" % str(main.status_label.text))
# Bild 3: Auswahl im UI sichtbar — Inspektor-Tab nach vorn.
var tab := _show_selection_tab(main)
await process_frame
await process_frame
print("PRUEF tab='%s'" % tab)
await _shot(dir + "/u5_3_ausgewaehlt_inspektor.png")
var ok := not bool(interaction.get("placing")) \
and placed == 1 \
and int(interaction.get("selected_index")) == 0 \
and hat_auswahl \
and str(main.status_label.text).contains("ausgewählt")
print("PROBE_U5_%s" % ("OK" if ok else "FAIL"))
quit(0 if ok else 1)

View file

@ -0,0 +1 @@
uid://c3cmwfhdm7vro

478
tests/test_place_once.gd Normal file
View file

@ -0,0 +1,478 @@
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": {"crate_wood": {"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", "crate_wood")
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": {"crate_wood": {"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", "crate_wood")
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", "crate_wood")
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()

View file

@ -0,0 +1 @@
uid://cl4x63iimyv82