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:
parent
403895b743
commit
f16b9c8fb6
5 changed files with 666 additions and 10 deletions
|
|
@ -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)
|
||||
|
|
|
|||
142
tests/probe_u5_place_once_render.gd
Normal file
142
tests/probe_u5_place_once_render.gd
Normal 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)
|
||||
1
tests/probe_u5_place_once_render.gd.uid
Normal file
1
tests/probe_u5_place_once_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c3cmwfhdm7vro
|
||||
478
tests/test_place_once.gd
Normal file
478
tests/test_place_once.gd
Normal 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()
|
||||
1
tests/test_place_once.gd.uid
Normal file
1
tests/test_place_once.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cl4x63iimyv82
|
||||
Loading…
Reference in a new issue