- R = Auswahl 90° drehen (Szene um Auswahl-Mitte, Bauen-Bauteil um Fußpunkt, Gruppe komplett; Umschalt+R = -90°), Mausrad-Feindrehen 15° bleibt - Pfeiltasten = 2-m-Raster-Verschiebung, Richtung = Kamera-Blickrichtung auf nächste Weltachse gerastet (MoveMath.nudge_delta, pure) - Bild↑/↓ = ±0,25 m auf der GANZEN Auswahl (EIN Undo), Strg+Bild↑ = Fußpunkt aufs Terrain/Boden absetzen - G = Greifen (Blender-Semantik): Auswahl hängt an der Maus, LMB setzt mit Raster-Snap, Esc rollt byte-genau zurück (ShortcutPolicy cancel_grab) - Terrain-Pinsel: [ / ] = Preset-Zyklus Klein/Mittel/Groß (Slider/Label/Preset-Buttons wandern mit), 1-4 = Heben/Senken/Glätten/Malen (PAINT-Clamp-Fix, Palette mit Malen sichtbar); Ziffern NUR im Terrain-Werkzeug - Strg halten = temporärer Snap-Zwang beim Bewegen/Greifen/Platzieren - Tests: test_shortcuts.gd 157 Checks (R byte-genau, Pfeil-Raster, Lift-Stufen, Boden-Snap, Greifen+Esc, Pinsel-Zyklus, History-Eintrag pro Operation), test_gui_smoke.gd erweitert (38 Checks) und in run_tests.sh aufgenommen; F-Regression seit Karte 1 damit abgedeckt - Suite 9/9 grün (696 Checks), Builder-Export 21/0, Parse-Check 11 Dateien grün
739 lines
No EOL
34 KiB
GDScript
739 lines
No EOL
34 KiB
GDScript
extends SceneTree
|
||
## Editor-Tests (headless): Karte t_c46d31c2 — Tastatur-Shortcuts 1/3
|
||
## (Freelook-Flug, Focus, Esc, Entf-Mehrfachauswahl, Strg+D, Strg+LMB).
|
||
## Ausführen: godot --headless --path . --script res://tests/test_shortcuts.gd
|
||
## Exit 0 = alles grün. Reine Mathe/Policy/Objekt-Tests + ECHTE interaction.gd
|
||
## gegen Doppel-Stub (Muster aus test_library.gd — kein UI nötig).
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
|
||
|
||
func _init() -> void:
|
||
ShortcutKeys.register()
|
||
_test_inputmap_actions()
|
||
_test_fly_math()
|
||
_test_esc_policy()
|
||
_test_esc_grab_policy()
|
||
_test_duplicate_offset()
|
||
_test_move_math()
|
||
# Node-basierte Tests (global_position, look_at) brauchen einen fertigen
|
||
# Baum — in _init ist root noch nicht "inside tree" (Gui-Smoke-Muster).
|
||
await process_frame
|
||
_test_multi_delete()
|
||
_test_focus_center()
|
||
_test_camera_fly_roundtrip()
|
||
_test_rotate_90()
|
||
_test_nudge_grid()
|
||
_test_lift_multi()
|
||
_test_snap_to_floor()
|
||
_test_grab()
|
||
_test_build_nudge_floor()
|
||
_test_brush_paint_mode()
|
||
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)
|
||
|
||
|
||
# --------------------------------------------------- Stubs (main / builder) --
|
||
|
||
## Minimales main-Doppel: interaction braucht doc/history/manifest + Status-
|
||
## und Sync-Aufrufe. Zählt Commits mit, damit „EIN Undo-Schritt" beweisbar ist.
|
||
class ProbeMain:
|
||
var doc: Dictionary = {}
|
||
var manifest: Dictionary = {}
|
||
var grid_snap := false
|
||
var dirty := false
|
||
var history: ActionHistory = ActionHistory.new()
|
||
var statuses: Array = []
|
||
|
||
func object_label(_oid: String) -> String:
|
||
return "Objekt"
|
||
|
||
func _fmt_int(v: int) -> String:
|
||
return str(v)
|
||
|
||
func _set_status(text: String, _err := false) -> void:
|
||
statuses.append(text)
|
||
|
||
func _selection_status(_idx: int) -> void:
|
||
pass
|
||
|
||
func _sync_selection_panel() -> void:
|
||
pass
|
||
|
||
func _mark_tree_selection(_idx: int) -> void:
|
||
pass
|
||
|
||
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
|
||
|
||
|
||
## Builder-Doppel: interaction setzt nur set_selected_multi / liest Marker.
|
||
class ProbeBuilder extends Node3D:
|
||
var object_markers: Array = []
|
||
var last_selection: Array = []
|
||
var last_gebaeude_selection := -99
|
||
|
||
func set_selected_multi(indices: Array) -> void:
|
||
last_selection = indices.duplicate()
|
||
|
||
func set_gebaeude_selected(idx: int) -> void:
|
||
last_gebaeude_selection = idx
|
||
|
||
|
||
# ------------------------------------------------------------ InputMap -------
|
||
|
||
func _test_inputmap_actions() -> void:
|
||
print("Testgruppe: InputMap-Actions (ShortcutKeys)")
|
||
# Idempotenz: doppeltes Registrieren darf keine Doppel-Events erzeugen.
|
||
ShortcutKeys.register()
|
||
var fwd_count := 0
|
||
for ev in InputMap.action_get_events("editor_fly_forward"):
|
||
fwd_count += 1
|
||
_check(fwd_count == 1, "register() ist idempotent (keine Doppel-Events)")
|
||
var expected_keys := {
|
||
"editor_fly_forward": KEY_W,
|
||
"editor_fly_back": KEY_S,
|
||
"editor_fly_left": KEY_A,
|
||
"editor_fly_right": KEY_D,
|
||
"editor_fly_up": KEY_E,
|
||
"editor_fly_down": KEY_Q,
|
||
"editor_fly_fast": KEY_SHIFT,
|
||
"editor_fly_slow": KEY_ALT,
|
||
"editor_focus_selection": KEY_F,
|
||
"editor_cancel": KEY_ESCAPE,
|
||
"editor_delete_selection": KEY_DELETE,
|
||
}
|
||
for action in expected_keys:
|
||
if not InputMap.has_action(action):
|
||
_check(false, "Action existiert: %s" % action)
|
||
continue
|
||
var ok := false
|
||
for ev in InputMap.action_get_events(action):
|
||
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == int(expected_keys[action]):
|
||
ok = true
|
||
_check(ok, "%s → physische Taste %s" % [action, OS.get_keycode_string(int(expected_keys[action]))])
|
||
# Modifier-Kombos
|
||
var toggle_shift := false
|
||
for ev in InputMap.action_get_events("editor_freelook_toggle"):
|
||
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == KEY_F \
|
||
and (ev as InputEventKey).shift_pressed:
|
||
toggle_shift = true
|
||
_check(toggle_shift, "editor_freelook_toggle → Umschalt+F")
|
||
var dup_ctrl := false
|
||
for ev in InputMap.action_get_events("editor_duplicate_selection"):
|
||
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == KEY_D \
|
||
and (ev as InputEventKey).ctrl_pressed:
|
||
dup_ctrl = true
|
||
_check(dup_ctrl, "editor_duplicate_selection → Strg+D")
|
||
var multi := false
|
||
for ev in InputMap.action_get_events("editor_multi_select"):
|
||
if ev is InputEventMouseButton and (ev as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT \
|
||
and (ev as InputEventMouseButton).ctrl_pressed:
|
||
multi = true
|
||
_check(multi, "editor_multi_select → Strg+LMB")
|
||
var freelook_rmb := false
|
||
for ev in InputMap.action_get_events("editor_freelook"):
|
||
if ev is InputEventMouseButton and (ev as InputEventMouseButton).button_index == MOUSE_BUTTON_RIGHT:
|
||
freelook_rmb = true
|
||
_check(freelook_rmb, "editor_freelook → Rechte Maustaste")
|
||
# Karte 2/3 (t_55257792): neue Actions mit physischen Tasten.
|
||
var expected_keys_2 := {
|
||
"editor_rotate_90": KEY_R,
|
||
"editor_nudge_up": KEY_UP,
|
||
"editor_nudge_down": KEY_DOWN,
|
||
"editor_nudge_left": KEY_LEFT,
|
||
"editor_nudge_right": KEY_RIGHT,
|
||
"editor_grab": KEY_G,
|
||
"editor_brush_smaller": KEY_BRACKETLEFT,
|
||
"editor_brush_larger": KEY_BRACKETRIGHT,
|
||
"editor_brush_raise": KEY_1,
|
||
"editor_brush_lower": KEY_2,
|
||
"editor_brush_smooth": KEY_3,
|
||
"editor_brush_paint": KEY_4,
|
||
}
|
||
for action in expected_keys_2:
|
||
if not InputMap.has_action(action):
|
||
_check(false, "Action existiert: %s" % action)
|
||
continue
|
||
var ok := false
|
||
for ev in InputMap.action_get_events(action):
|
||
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == int(expected_keys_2[action]):
|
||
ok = true
|
||
_check(ok, "%s → physische Taste %s" % [action, OS.get_keycode_string(int(expected_keys_2[action]))])
|
||
var lift_floor := false
|
||
for ev in InputMap.action_get_events("editor_lift_to_floor"):
|
||
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == KEY_PAGEUP \
|
||
and (ev as InputEventKey).ctrl_pressed:
|
||
lift_floor = true
|
||
_check(lift_floor, "editor_lift_to_floor → Strg+Bild↑")
|
||
|
||
|
||
# ------------------------------------------------------------ FlyMath --------
|
||
|
||
func _test_fly_math() -> void:
|
||
print("Testgruppe: Freelook-Mathe (FlyMath)")
|
||
_check(absf(FlyMath.speed_for(30.0, false, false) - 30.0) < 0.0001, "Tempo normal = Grund")
|
||
_check(absf(FlyMath.speed_for(30.0, true, false) - 90.0) < 0.0001, "Umschalt = ×3")
|
||
_check(absf(FlyMath.speed_for(30.0, false, true) - 9.0) < 0.0001, "Alt = ×0,3")
|
||
_check(absf(FlyMath.speed_for(30.0, true, true) - 90.0) < 0.0001, "Umschalt dominiert bei beiden")
|
||
_check(absf(FlyMath.wheel_speed(30.0, true) - 34.5) < 0.0001, "Mausrad hoch: ×1,15 dauerhaft")
|
||
_check(absf(FlyMath.wheel_speed(30.0, false) - 30.0 / 1.15) < 0.0001, "Mausrad runter: ÷1,15 dauerhaft")
|
||
_check(FlyMath.wheel_speed(299.0, true) == FlyMath.SPEED_MAX_MPS, "Mausrad: Tempo-Grenze oben (300 m/s)")
|
||
_check(FlyMath.wheel_speed(2.0, false) == FlyMath.SPEED_MIN_MPS, "Mausrad: Tempo-Grenze unten (2 m/s)")
|
||
# Bewegung: W fliegt in Blickrichtung (3D), A/D horizontal, E/Q weltachsig.
|
||
var fwd := FlyMath.forward_from_euler(0.0, 30.0) # Blick nach -Z, leicht abwärts
|
||
var right := FlyMath.forward_from_euler(0.0, 0.0).cross(Vector3.UP).normalized()
|
||
var d := FlyMath.move_delta(fwd, right, 1.0, 0.0, 0.0, 10.0, 1.0)
|
||
_check(d.length() < 10.0001 and absf(d.y) > 0.1, "W fliegt 3D in Blickrichtung (mit Pitch-Anteil)")
|
||
var side := FlyMath.move_delta(fwd, right, 0.0, 1.0, 0.0, 10.0, 1.0)
|
||
_check(absf(side.y) < 0.0001 and absf(side.x - 10.0) < 0.0001, "D straft horizontal rechts (kein Höhen-Anteil)")
|
||
var up := FlyMath.move_delta(fwd, right, 0.0, 0.0, 1.0, 10.0, 1.0)
|
||
_check(absf((up - Vector3.UP * 10.0).length()) < 0.0001, "E hebt weltachsig (+10 m)")
|
||
var diag := FlyMath.move_delta(fwd, right, 1.0, 1.0, 0.0, 10.0, 1.0)
|
||
_check(absf(diag.length() - 10.0) < 0.0001, "Diagonale normalisiert (√2-Falle umgangen)")
|
||
_check(FlyMath.move_delta(fwd, right, 0.0, 0.0, 0.0, 10.0, 1.0) == Vector3.ZERO, "keine Eingabe → kein Delta")
|
||
# Euler ↔ Forward-Rundtrip (Orbit-Konvention: pitch + = von oben).
|
||
var ep := FlyMath.euler_from_forward(FlyMath.forward_from_euler(45.0, 35.0))
|
||
_check(absf(ep.x - 45.0) < 0.0001 and absf(ep.y - 35.0) < 0.0001, "Euler↔Forward-Rundtrip (45°/35°)")
|
||
var down := FlyMath.euler_from_forward(Vector3(0.0, -1.0, 0.0))
|
||
_check(absf(down.y - 90.0) < 0.0001, "Blick senkrecht nach unten → pitch +90 (Orbit-Clamp übernimmt)")
|
||
var flat := FlyMath.forward_from_euler(0.0, 0.0)
|
||
_check(absf((flat - Vector3(0.0, 0.0, -1.0)).length()) < 0.0001, "yaw 0/pitch 0 → Blick -Z")
|
||
|
||
|
||
# ------------------------------------------------------------ Esc-Policy -----
|
||
|
||
func _test_esc_policy() -> void:
|
||
print("Testgruppe: Esc-Zustandswechsel (ShortcutPolicy)")
|
||
_check(ShortcutPolicy.esc_action("terrain", false, false) == "exit_terrain", "Esc im Terrain-Werkzeug → Werkzeug verlassen")
|
||
_check(ShortcutPolicy.esc_action("spawns", false, false) == "exit_spawns", "Esc im Spawn-Werkzeug → Werkzeug verlassen")
|
||
_check(ShortcutPolicy.esc_action("bauen", false, false) == "exit_bauen", "Esc im Bauen-Modus → Werkzeug verlassen")
|
||
_check(ShortcutPolicy.esc_action("bauen", true, true) == "exit_bauen", "Werkzeug hat VORRANG vor Platzieren")
|
||
_check(ShortcutPolicy.esc_action("objects", true, true) == "cancel_placement", "Esc beim Platzieren → Ghost-Preview abbrechen")
|
||
_check(ShortcutPolicy.esc_action("objects", false, true) == "clear_selection", "Esc mit Auswahl → Auswahl aufheben")
|
||
_check(ShortcutPolicy.esc_action("objects", false, false) == "none", "Esc ohne nichts → kein Zustandswechsel")
|
||
|
||
|
||
# ------------------------------------------------------ Duplizieren (+2 m) ---
|
||
|
||
func _test_duplicate_offset() -> void:
|
||
print("Testgruppe: Strg+D — Duplizieren +2 m X, Füßpunkt unverändert")
|
||
var doc := {"objects": [
|
||
{"id": "crate_wood", "pos": [10.0, 5.0, 10.0], "scale": 1.0, "rot_y_deg": 30.0},
|
||
{"id": "tree_pine", "pos": [30.0, 3.0, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]}
|
||
var hist := ActionHistory.new()
|
||
hist.begin(doc)
|
||
var before := hist.snapshot(["objects"])
|
||
var ni: Array = ObjectOps.duplicate_objects(doc, [0, 1], false)
|
||
_check(ni == [2, 3], "Mehrfach-Duplizieren: neue Indizes in Quell-Reihenfolge")
|
||
var objects: Array = doc["objects"]
|
||
_check(float(objects[2]["pos"][0]) == 12.0 and float(objects[2]["pos"][2]) == 10.0,
|
||
"Kopie 1: +2 m in X, Z unverändert")
|
||
_check(float(objects[2]["pos"][1]) == 5.0, "Kopie 1: Füßpunkt-Y unverändert")
|
||
_check(absf(float(objects[2]["rot_y_deg"]) - 30.0) < 0.0001, "Kopie 1: Drehung übernommen")
|
||
_check(float(objects[3]["pos"][0]) == 32.0 and float(objects[3]["pos"][1]) == 3.0,
|
||
"Kopie 2: +2 m in X, Höhe unverändert")
|
||
# Raster-Snap: 11,6 rastet auf 12,2 m? Nein: auf ganze Meter → 14 (11.6+2=13.6→14).
|
||
objects[0]["pos"] = [11.6, 2.0, 9.2]
|
||
var idx := ObjectOps.duplicate_object(doc, 0, true)
|
||
_check(float(objects[idx]["pos"][0]) == 14.0 and float(objects[idx]["pos"][2]) == 9.0,
|
||
"Raster-Snap an: Kopie rastet auf ganze Meter (nach +2 m)")
|
||
# EIN Undo-Schritt für ALLE Kopien:
|
||
hist.commit("3 Objekte duplizieren", before, hist.snapshot(["objects"]))
|
||
hist.undo()
|
||
_check((doc["objects"] as Array).size() == 2, "Strg+Z: ALLE Kopien auf einmal weg")
|
||
hist.redo()
|
||
_check((doc["objects"] as Array).size() == 5, "Strg+Y: ALLE Kopien auf einmal zurück")
|
||
|
||
|
||
# ---------------------------------------------------- Del-Mehrfachauswahl ----
|
||
|
||
func _test_multi_delete() -> void:
|
||
print("Testgruppe: Entf auf Mehrfachauswahl (echte interaction.gd)")
|
||
var fake := ProbeMain.new()
|
||
fake.doc = {"objects": [
|
||
{"id": "crate_wood", "pos": [0.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "tree_pine", "pos": [10.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "crate_wood", "pos": [20.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "tree_pine", "pos": [30.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]}
|
||
fake.history.begin(fake.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
|
||
var builder := ProbeBuilder.new()
|
||
vp.add_child(builder)
|
||
var interaction := Node3D.new()
|
||
interaction.set_script(load("res://src/editor/interaction.gd"))
|
||
vp.add_child(interaction)
|
||
interaction.setup(fake, cam, builder)
|
||
|
||
# Strg+Klick-Mehrfachauswahl-Semantik (select_toggle = add/remove):
|
||
interaction.select_object(0)
|
||
interaction.select_toggle(2)
|
||
_check(interaction.get("selected_indices") == [0, 2], "Umschalt/Strg+Klick: Objekt zur Auswahl hinzugefügt")
|
||
interaction.select_toggle(2)
|
||
_check(interaction.get("selected_indices") == [0], "Umschalt/Strg+Klick: wieder entfernt")
|
||
interaction.select_toggle(2)
|
||
_check(interaction.get("selected_indices") == [0, 2], "Mehrfachauswahl [0, 2] steht")
|
||
|
||
# Entf: beide gelöscht, EIN Undo-Schritt holt beide zurück.
|
||
interaction.delete_selected()
|
||
var objects: Array = fake.doc["objects"]
|
||
_check(objects.size() == 2, "Entf: beide ausgewählten Objekte weg (2 bleiben)")
|
||
_check(str(objects[0]["id"]) == "tree_pine" and str(objects[1]["id"]) == "tree_pine",
|
||
"Entf: die RICHTIGEN Objekte (A und C) sind weg")
|
||
_check(interaction.get("selected_indices").is_empty(), "Entf: Auswahl geleert")
|
||
_check(builder.last_selection.is_empty(), "Entf: auch Viewport-Markierung geleert")
|
||
_check(fake.history.can_undo(), "Entf: genau EIN History-Eintrag")
|
||
fake.history.undo()
|
||
_check((fake.doc["objects"] as Array).size() == 4, "Strg+Z: beide Objekte auf einmal zurück")
|
||
|
||
# Entf ohne Auswahl: hilfreiche Meldung, kein Crash, keine History-Änderung.
|
||
var can_undo_before := fake.history.can_undo()
|
||
var status_before: int = fake.statuses.size()
|
||
interaction.clear_selection(true)
|
||
interaction.delete_selected()
|
||
_check(fake.statuses.size() > status_before, "Entf ohne Auswahl: hilfreiche Meldung statt Stille")
|
||
_check(fake.history.can_undo() == can_undo_before, "Entf ohne Auswahl: nichts committet")
|
||
|
||
# Mausrad-Block im Flugmodus: wheel_blocked meldet Kamera-Vorrang.
|
||
var orbit := Camera3D.new()
|
||
orbit.set_script(load("res://src/viewport/orbit_camera.gd"))
|
||
vp.add_child(orbit)
|
||
interaction.set("camera", orbit)
|
||
_check(not bool(interaction.call("wheel_blocked")), "Orbit + Auswahl: Mausrad dreht Objekt (normal)")
|
||
orbit.set("_flying", true)
|
||
_check(bool(interaction.call("wheel_blocked")), "Freelook-Flug: Mausrad gehört dem Flug-Tempo")
|
||
|
||
|
||
# ------------------------------------------------------------ Focus (F) ------
|
||
|
||
func _test_focus_center() -> void:
|
||
print("Testgruppe: F — Focus-Selection-Mittelpunkt (SelectionMath)")
|
||
var m1 := Node3D.new()
|
||
m1.position = Vector3(10.0, 0.0, 0.0)
|
||
m1.set_meta("half_height", 2.0)
|
||
var m2 := Node3D.new()
|
||
m2.position = Vector3(30.0, 4.0, 20.0)
|
||
m2.set_meta("half_height", 2.0)
|
||
root.add_child(m1)
|
||
root.add_child(m2)
|
||
var center := SelectionMath.focus_center([m1, m2], [0, 1])
|
||
# Anker + halbe Mesh-Höhe: m1 → (10, 1, 0), m2 → (30, 5, 20); Mittel = (20, 3, 10).
|
||
_check((center - Vector3(20.0, 3.0, 10.0)).length() < 0.0001,
|
||
"Mittelpunkt = Mittel der Anker (+ halbe Mesh-Höhe)")
|
||
_check(SelectionMath.focus_center([m1, m2], [5, -1]) == Vector3.ZERO,
|
||
"ungültige Indizes → Ursprung (Guard sitzt beim Aufrufer)")
|
||
_check(SelectionMath.focus_center([null, m2], [0, 1]) == (m2.global_position \
|
||
+ Vector3.UP * 1.0), "kaputter Marker wird übersprungen")
|
||
|
||
|
||
# ------------------------------------------------- Kamera Flug-Roundtrip -----
|
||
|
||
func _test_camera_fly_roundtrip() -> void:
|
||
print("Testgruppe: Freelook-Flug an/aus (orbit_camera, kein Positions-Sprung)")
|
||
var cam := Camera3D.new()
|
||
cam.set_script(load("res://src/viewport/orbit_camera.gd"))
|
||
root.add_child(cam)
|
||
cam.set("target", Vector3.ZERO)
|
||
cam.set("distance", 100.0)
|
||
cam.set("yaw_deg", 30.0)
|
||
cam.set("pitch_deg", 40.0)
|
||
cam.call("_apply")
|
||
var pos_before: Vector3 = cam.position
|
||
_check(not bool(cam.call("fly_active")), "Orbit-Modus ist Default (kein Flug)")
|
||
cam.call("_enter_fly")
|
||
_check(bool(cam.call("fly_active")), "RMT gedrückt → Flugmodus aktiv")
|
||
cam.position += Vector3(25.0, -10.0, 5.0) # im Flug frei bewegt
|
||
cam.call("_exit_fly")
|
||
_check(not bool(cam.call("fly_active")), "RMT losgelassen → zurück zum Orbit")
|
||
var pos_after: Vector3 = cam.position
|
||
# Orbit-Ziel wurde aus der Flugposition zurückgerechnet — die KAMERA bleibt,
|
||
# nur Ziel/Winkel ändern sich: kein Teleport beim Umschalten.
|
||
_check((pos_after - (pos_before + Vector3(25.0, -10.0, 5.0))).length() < 0.01,
|
||
"Flug-Exit: Position bleibt (kein Sprung), Ziel wird zurückgerechnet")
|
||
# Focus in Orbit-Mode: Ziel wandert auf den Mittelpunkt.
|
||
cam.call("focus_on", Vector3(50.0, 2.0, -20.0))
|
||
_check((cam.get("target") as Vector3 - Vector3(50.0, 2.0, -20.0)).length() < 0.0001,
|
||
"focus_on: Orbit-Ziel = Auswahl-Mittelpunkt")
|
||
# Umschalt+F-Verriegelung: Dauerflug an → aus.
|
||
cam.call("toggle_fly_lock")
|
||
_check(bool(cam.call("fly_active")), "Umschalt+F: Dauerflug an (ohne RMT)")
|
||
cam.call("toggle_fly_lock")
|
||
_check(not bool(cam.call("fly_active")), "Umschalt+F nochmal: zurück zum Orbit")
|
||
# Blick-Yaw-Accessor (Karte t_55257792): Orbit → yaw_deg, Flug → Flug-Yaw.
|
||
_check(absf(float(cam.call("view_yaw_deg")) - 30.0) < 0.0001,
|
||
"view_yaw_deg im Orbit = Orbit-Yaw (30°)")
|
||
cam.set("_fly_yaw", -120.0)
|
||
cam.set("_flying", true)
|
||
_check(absf(float(cam.call("view_yaw_deg")) + 120.0) < 0.0001,
|
||
"view_yaw_deg im Flug = Flug-Yaw")
|
||
|
||
|
||
# --------------------------------------------------------- Esc + Greifen -----
|
||
|
||
func _test_esc_grab_policy() -> void:
|
||
print("Testgruppe: Esc-Priorität mit Greifen (ShortcutPolicy)")
|
||
_check(ShortcutPolicy.esc_action("objects", false, false, true) == "cancel_grab",
|
||
"Esc beim Greifen → Greifen zurückrollen")
|
||
_check(ShortcutPolicy.esc_action("objects", false, true, true) == "cancel_grab",
|
||
"Greifen hat VORRANG vor Auswahl-Aufheben")
|
||
_check(ShortcutPolicy.esc_action("objects", true, true, true) == "cancel_grab",
|
||
"Greifen hat VORRANG vor Platzieren (kann nicht koexistieren)")
|
||
_check(ShortcutPolicy.esc_action("terrain", false, false, true) == "exit_terrain",
|
||
"Werkzeug-Exit bleibt VOR Greifen (im Terrain gibt es kein Greifen)")
|
||
_check(ShortcutPolicy.esc_action("objects", true, true, false) == "cancel_placement",
|
||
"alter 3-Argument-Aufruf bleibt unverändert (Rückwärtskompatibel)")
|
||
|
||
|
||
# ------------------------------------------------------ MoveMath (Karte 2) ----
|
||
|
||
func _test_move_math() -> void:
|
||
print("Testgruppe: Pfeil-Raster + Pinsel-Presets (MoveMath)")
|
||
# Blick -Z (yaw 0): ↑ = -Z, ↓ = +Z, rechts = +X, links = -X.
|
||
_check(MoveMath.nudge_delta(0.0, 1, 0) == Vector3(0, 0, -2), "yaw 0: ↑ = 2 m nach -Z")
|
||
_check(MoveMath.nudge_delta(0.0, -1, 0) == Vector3(0, 0, 2), "yaw 0: ↓ = 2 m nach +Z")
|
||
_check(MoveMath.nudge_delta(0.0, 0, 1) == Vector3(2, 0, 0), "yaw 0: → = 2 m nach +X")
|
||
_check(MoveMath.nudge_delta(0.0, 0, -1) == Vector3(-2, 0, 0), "yaw 0: ← = 2 m nach -X")
|
||
# Blick -X (yaw 90): ↑ = -X, rechts = -Z (rechts vom Blick).
|
||
_check(MoveMath.nudge_delta(90.0, 1, 0) == Vector3(-2, 0, 0), "yaw 90: ↑ = 2 m nach -X")
|
||
_check(MoveMath.nudge_delta(90.0, 0, 1) == Vector3(0, 0, -2), "yaw 90: → = 2 m nach -Z")
|
||
# Blick +Z (yaw 180): ↑ = +Z.
|
||
_check(MoveMath.nudge_delta(180.0, 1, 0) == Vector3(0, 0, 2), "yaw 180: ↑ = 2 m nach +Z")
|
||
# Blick +X (yaw -90): ↑ = +X, → = +Z.
|
||
_check(MoveMath.nudge_delta(-90.0, 1, 0) == Vector3(2, 0, 0), "yaw -90: ↑ = 2 m nach +X")
|
||
_check(MoveMath.nudge_delta(-90.0, 0, 1) == Vector3(0, 0, 2), "yaw -90: → = 2 m nach +Z")
|
||
# Schräger Blick rastet auf die NÄCHSTE Weltachse (Editor-Konvention):
|
||
# yaw 20 → Blick überwiegend -Z → ↑ = -Z; yaw 70 → überwiegend -X → ↑ = -X.
|
||
_check(MoveMath.nudge_delta(20.0, 1, 0) == Vector3(0, 0, -2), "yaw 20 (schräg): ↑ rastet auf -Z")
|
||
_check(MoveMath.nudge_delta(70.0, 1, 0) == Vector3(-2, 0, 0), "yaw 70 (schräg): ↑ rastet auf -X")
|
||
# Pinsel-Presets: Zyklus Klein(4) → Mittel(12) → Groß(32) → Klein ...
|
||
_check(MoveMath.brush_next_preset(4.0, true) == 12.0, "[ ]-Zyklus: Klein → Mittel")
|
||
_check(MoveMath.brush_next_preset(12.0, true) == 32.0, "[ ]-Zyklus: Mittel → Groß")
|
||
_check(MoveMath.brush_next_preset(32.0, true) == 4.0, "[ ]-Zyklus: Groß → Klein (wrap)")
|
||
_check(MoveMath.brush_next_preset(32.0, false) == 12.0, "[ ]-Zyklus zurück: Groß → Mittel")
|
||
_check(MoveMath.brush_next_preset(4.0, false) == 32.0, "[ ]-Zyklus zurück: Klein → Groß (wrap)")
|
||
# Freier Slider-Wert: nächstes Preset ÜBER bzw. UNTER dem Wert.
|
||
_check(MoveMath.brush_next_preset(20.0, true) == 32.0, "freier Wert 20: ] → nächstes Preset darüber (32)")
|
||
_check(MoveMath.brush_next_preset(20.0, false) == 12.0, "freier Wert 20: [ → nächstes Preset darunter (12)")
|
||
|
||
|
||
# --------------------------------------------------------- R = 90°-Drehen ----
|
||
|
||
func _make_interaction(doc: Dictionary, markers: int) -> Array:
|
||
var fake := ProbeMain.new()
|
||
fake.doc = doc
|
||
fake.history.begin(fake.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
|
||
var builder := ProbeBuilder.new()
|
||
vp.add_child(builder)
|
||
for i in markers:
|
||
var m := Node3D.new()
|
||
m.set_meta("half_height", 1.0)
|
||
builder.object_markers.append(m)
|
||
var interaction := Node3D.new()
|
||
interaction.set_script(load("res://src/editor/interaction.gd"))
|
||
vp.add_child(interaction)
|
||
interaction.setup(fake, cam, builder)
|
||
return [fake, interaction, builder]
|
||
|
||
|
||
func _test_rotate_90() -> void:
|
||
print("Testgruppe: R = Auswahl 90° drehen (byte-genau)")
|
||
var doc := {"objects": [
|
||
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 30.0},
|
||
{"id": "tree_pine", "pos": [20.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "crate_wood", "pos": [10.0, 0.0, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]}
|
||
var pair := _make_interaction(doc, 3)
|
||
var fake: ProbeMain = pair[0]
|
||
var interaction = pair[1]
|
||
# Einzelnes Objekt: R dreht rot_y_deg exakt +90, Position bleibt (Drehung
|
||
# um die eigene Mitte = Fußpunkt bleibt an Ort und Stelle).
|
||
interaction.select_object(0)
|
||
interaction.rotate_selected(90.0)
|
||
var objects: Array = doc["objects"]
|
||
_check(absf(float(objects[0]["rot_y_deg"]) - 120.0) < 0.0001, "R: 30° → 120° (exakt +90)")
|
||
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 10.0,
|
||
"R: Einzelobjekt bleibt auf seinem Fußpunkt stehen")
|
||
_check(float(objects[0]["pos"][1]) == 0.0, "R: Füßpunkt-Y byte-genau unverändert")
|
||
_check(fake.history.can_undo(), "R: EIN History-Eintrag pro Drehung")
|
||
# Mehrfachauswahl: beide drehen um die GEMEINSAME Mitte — beide rotieren
|
||
# +90, die Mitte bleibt (byte-genau: gleicher Code-Pfad wie Mausrad).
|
||
interaction.select_many([0, 2])
|
||
var c_before := GroupOps.centroid(doc, [0, 2])
|
||
interaction.rotate_selected(90.0)
|
||
var c_after := GroupOps.centroid(doc, [0, 2])
|
||
_check(absf(float(objects[0]["rot_y_deg"]) - 210.0) < 0.0001
|
||
and absf(float(objects[2]["rot_y_deg"]) - 90.0) < 0.0001,
|
||
"R bei Mehrfachauswahl: beide Objekte +90°")
|
||
_check((c_after - c_before).length() < 0.0001,
|
||
"R: Auswahl-Mittelpunkt bleibt exakt (Drehung um die Mitte)")
|
||
# EIN History-Eintrag pro Operation: das erste Undo nimmt nur die
|
||
# Mehrfach-Drehung zurück (210°→120°, Fußpunkte wieder am alten Ort),
|
||
# das zweite die Einzel-Drehung davor (120°→30°).
|
||
fake.history.undo()
|
||
_check(absf(float(objects[0]["rot_y_deg"]) - 120.0) < 0.0001
|
||
and absf(float(objects[2]["rot_y_deg"])) < 0.0001
|
||
and float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 10.0
|
||
and float(objects[2]["pos"][0]) == 10.0 and float(objects[2]["pos"][2]) == 20.0,
|
||
"Strg+Z: Mehrfach-Drehung komplett zurück (Rotation + Fußpunkte byte-genau)")
|
||
fake.history.undo()
|
||
_check(absf(float(objects[0]["rot_y_deg"]) - 30.0) < 0.0001,
|
||
"Strg+Z: zweite Ebene — Einzel-Drehung auch zurück (30°)")
|
||
# Bauen-Modus: Bauteil rotiert um seinen Fußpunkt (Position unverändert).
|
||
var build_doc := {"gebaeude": [
|
||
{"id": "wand", "pos": [4.0, 0.0, 6.0], "rot": 0.0, "punkte": [[0, 0, 0], [2, 0, 0]]},
|
||
]}
|
||
var bfake := ProbeMain.new()
|
||
bfake.doc = build_doc
|
||
bfake.history.begin(bfake.doc)
|
||
var bbuilder := ProbeBuilder.new()
|
||
root.add_child(bbuilder)
|
||
var btool := Node3D.new()
|
||
btool.set_script(load("res://src/editor/build_tool.gd"))
|
||
root.add_child(btool)
|
||
btool.setup(bfake, Camera3D.new(), bbuilder, interaction)
|
||
btool.select_index(0)
|
||
btool.rotate_input()
|
||
var list: Array = BuildOps.pieces(build_doc)
|
||
_check(absf(float(list[0]["rot"]) - 90.0) < 0.0001, "Bauen: R dreht Bauteil 90°")
|
||
_check(float(list[0]["pos"][0]) == 4.0 and float(list[0]["pos"][2]) == 6.0,
|
||
"Bauen: Fußpunkt bleibt (Drehung um den Fußpunkt)")
|
||
|
||
|
||
# ---------------------------------------------------- Pfeiltasten 2-m-Raster --
|
||
|
||
func _test_nudge_grid() -> void:
|
||
print("Testgruppe: Pfeiltasten — 2-m-Raster, Höhe wandert mit, EIN Undo pro Druck")
|
||
var doc := {"objects": [
|
||
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "tree_pine", "pos": [10.0, 3.0, 12.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]}
|
||
var pair := _make_interaction(doc, 2)
|
||
var fake: ProbeMain = pair[0]
|
||
var interaction = pair[1]
|
||
var objects: Array = doc["objects"]
|
||
interaction.select_object(0)
|
||
interaction.nudge_selection(Vector3(0, 0, -2))
|
||
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 8.0,
|
||
"↑: exakt 2 m in -Z, Fußpunkt bleibt im Raster")
|
||
_check(fake.history.can_undo(), "Pfeiltaste: EIN History-Eintrag")
|
||
fake.history.undo()
|
||
_check(float(objects[0]["pos"][2]) == 10.0, "Strg+Z: eine Zelle zurück")
|
||
# Höhe wandert mit (t_7567a06f-Semantik): angehobenes Objekt (y=3 über
|
||
# flachem Terrain) bleibt exakt auf 3 m.
|
||
interaction.select_object(1)
|
||
interaction.nudge_selection(Vector3(2, 0, 0))
|
||
_check(float(objects[1]["pos"][1]) == 3.0, "Pfeiltaste: Terrain-Abstand bleibt (3 m über flachem Boden)")
|
||
_check(float(objects[1]["pos"][0]) == 12.0, "Pfeiltaste: 2 m in +X")
|
||
# Mehrfachauswahl wandert gemeinsam, relative Anordnung exakt erhalten.
|
||
interaction.select_many([0, 1])
|
||
var d0: Array = [float(objects[0]["pos"][0]) - float(objects[1]["pos"][0]),
|
||
float(objects[0]["pos"][2]) - float(objects[1]["pos"][2])]
|
||
interaction.nudge_selection(Vector3(-2, 0, 0))
|
||
var d1: Array = [float(objects[0]["pos"][0]) - float(objects[1]["pos"][0]),
|
||
float(objects[0]["pos"][2]) - float(objects[1]["pos"][2])]
|
||
_check(absf(d0[0] - d1[0]) < 0.0001 and absf(d0[1] - d1[1]) < 0.0001,
|
||
"Mehrfachauswahl: relative Anordnung byte-genau erhalten")
|
||
# Map-Rand: außerhalb klemmt — komplett geklemmter Druck committet NICHT.
|
||
var small := {"terrain": {"size_x_m": 100.0, "size_z_m": 100.0, "resolution": 0},
|
||
"objects": [{"id": "crate_wood", "pos": [50.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0}]}
|
||
var pair2 := _make_interaction(small, 1)
|
||
var fake2: ProbeMain = pair2[0]
|
||
var interaction2 = pair2[1]
|
||
var sobjects: Array = small["objects"]
|
||
interaction2.select_object(0)
|
||
var had_undo: bool = fake2.history.can_undo()
|
||
interaction2.nudge_selection(Vector3(2, 0, 0))
|
||
_check(float(sobjects[0]["pos"][0]) == 50.0 and fake2.history.can_undo() == had_undo,
|
||
"Map-Rand: komplett geklemmte Zelle → keine Bewegung, KEIN History-Eintrag")
|
||
|
||
|
||
# ------------------------------------------- Bild↑/↓ Mehrfach + Boden-Snap ----
|
||
|
||
func _test_lift_multi() -> void:
|
||
print("Testgruppe: Bild↑/↓ auf der GANZEN Auswahl (EIN Undo-Schritt)")
|
||
var doc := {"objects": [
|
||
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "tree_pine", "pos": [20.0, 0.5, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]}
|
||
var pair := _make_interaction(doc, 2)
|
||
var interaction = pair[1]
|
||
var objects: Array = doc["objects"]
|
||
interaction.select_many([0, 1])
|
||
interaction.lift_selected(ObjectOps.LIFT_STEP_M)
|
||
_check(absf(float(objects[0]["pos"][1]) - 0.25) < 0.0001,
|
||
"Bild↑: Objekt 1 +0,25 m")
|
||
_check(absf(float(objects[1]["pos"][1]) - 0.75) < 0.0001,
|
||
"Bild↑: Objekt 2 +0,25 m (0,5 → 0,75)")
|
||
var marker_y: float = (pair[2].object_markers[0] as Node3D).position.y
|
||
_check(absf(marker_y - 0.25) < 0.0001, "Bild↑: Marker wandert mit")
|
||
interaction.lift_selected(-ObjectOps.LIFT_STEP_M)
|
||
_check(absf(float(objects[0]["pos"][1])) < 0.0001, "Bild↓: wieder −0,25 m")
|
||
# Objekt am Boden: weiter absenken geht nicht — kein Commit, Meldung kommt.
|
||
var can_undo: bool = (pair[0].history as ActionHistory).can_undo()
|
||
interaction.lift_selected(-ObjectOps.LIFT_STEP_M)
|
||
_check(absf(float(objects[0]["pos"][1])) < 0.0001, "Bild↓ am Boden: y bleibt 0")
|
||
_check((pair[0].history as ActionHistory).can_undo() == can_undo,
|
||
"Bild↓ am Boden: nichts committet (Snapshot verworfen)")
|
||
|
||
|
||
func _test_snap_to_floor() -> void:
|
||
print("Testgruppe: Strg+Bild↑ — Auswahl auf den Boden setzen")
|
||
var doc := {"objects": [
|
||
{"id": "crate_wood", "pos": [10.0, 3.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "tree_pine", "pos": [20.0, 0.0, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]}
|
||
var pair := _make_interaction(doc, 2)
|
||
var fake: ProbeMain = pair[0]
|
||
var interaction = pair[1]
|
||
var objects: Array = doc["objects"]
|
||
interaction.select_many([0, 1])
|
||
interaction.snap_selection_to_floor()
|
||
_check(float(objects[0]["pos"][1]) == 0.0, "Strg+Bild↑: schwebendes Objekt auf Terrain-Höhe (0 m)")
|
||
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 10.0,
|
||
"Strg+Bild↑: X/Z bleiben unangetastet")
|
||
_check(fake.history.can_undo(), "Strg+Bild↑: EIN History-Eintrag")
|
||
# Zweiter Druck: alles steht schon am Boden → keine Aktion, kein Eintrag.
|
||
var can_undo: bool = fake.history.can_undo()
|
||
interaction.snap_selection_to_floor()
|
||
_check(fake.history.can_undo() == can_undo, "Strg+Bild↑ am Boden: kein leerer Commit")
|
||
|
||
|
||
# --------------------------------------------------------------- Greifen (G) --
|
||
|
||
func _test_grab() -> void:
|
||
print("Testgruppe: G — Greifen mit Esc-Rückabwicklung (byte-genau)")
|
||
var doc := {"objects": [
|
||
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "tree_pine", "pos": [12.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]}
|
||
var pair := _make_interaction(doc, 2)
|
||
var fake: ProbeMain = pair[0]
|
||
var interaction = pair[1]
|
||
var builder: ProbeBuilder = pair[2]
|
||
var objects: Array = doc["objects"]
|
||
fake.grid_snap = true
|
||
interaction.select_many([0, 1])
|
||
var ok: bool = interaction.grab_begin()
|
||
_check(ok, "G mit Auswahl: Greifen startet")
|
||
# Linksklick-Setzen: Primär (letzter Auswahl-Index = 1) rastet auf ganze
|
||
# Meter, das andere Auswahl-Objekt wandert exakt mit demselben Delta mit.
|
||
interaction.grab_move(Vector3(20.3, 0.0, 8.6))
|
||
interaction.grab_confirm()
|
||
_check(float(objects[1]["pos"][0]) == 20.0 and float(objects[1]["pos"][2]) == 9.0,
|
||
"Greifen + LMB: Primär rastet aufs Raster (20, 9)")
|
||
_check(float(objects[0]["pos"][0]) == 18.0 and float(objects[0]["pos"][2]) == 9.0,
|
||
"Greifen: zweites Auswahl-Objekt wandert exakt mit (+8, −1)")
|
||
_check(float(objects[1]["pos"][1]) == 0.0, "Greifen: Füßpunkt auf Terrain-Höhe")
|
||
_check(fake.history.can_undo(), "Greifen: EIN History-Eintrag für den ganzen Zug")
|
||
fake.history.undo()
|
||
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[1]["pos"][0]) == 12.0,
|
||
"Strg+Z: Greifen-Zug komplett zurück")
|
||
# Esc-Rückabwicklung: Positionen byte-identisch zum Zustand vor dem Greifen.
|
||
interaction.grab_begin()
|
||
interaction.grab_move(Vector3(30.7, 0.0, 1.2))
|
||
_check(float(objects[1]["pos"][0]) == 31.0, "Greifen: live am Cursor (rastet)")
|
||
var can_undo: bool = fake.history.can_undo()
|
||
interaction.grab_cancel()
|
||
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][1]) == 0.0
|
||
and float(objects[0]["pos"][2]) == 10.0,
|
||
"Esc: exakte Rückabwicklung auf die Ausgangsposition (byte-identisch)")
|
||
_check(float(objects[1]["pos"][0]) == 12.0 and float(objects[1]["pos"][2]) == 10.0,
|
||
"Esc: auch das zweite Objekt zurück")
|
||
_check(fake.history.can_undo() == can_undo, "Esc: KEIN History-Eintrag beim Abbruch")
|
||
_check(not bool(interaction.get("_grabbing")), "Esc: Greifen-Zustand beendet")
|
||
# Marker sind nach dem Abbruch wieder synchron (Viewport = Doc).
|
||
_check(absf((builder.object_markers[0] as Node3D).position.x - 10.0) < 0.0001,
|
||
"Esc: Viewport-Marker zurückgesetzt")
|
||
# G ohne Auswahl: hilfreiche Meldung, kein Crash.
|
||
interaction.clear_selection(true)
|
||
_check(not interaction.grab_begin(), "G ohne Auswahl: Greifen startet nicht")
|
||
_check(fake.statuses.size() > 0, "G ohne Auswahl: hilfreiche Meldung statt Stille")
|
||
|
||
|
||
# ------------------------------------------- Bauen: Pfeile + Strg+Bild↑ -------
|
||
|
||
func _test_build_nudge_floor() -> void:
|
||
print("Testgruppe: Bauen-Modus — Pfeiltasten (2-m-Raster) + Strg+Bild↑")
|
||
var doc := {"gebaeude": [
|
||
{"id": "wand", "pos": [4.0, 0.0, 6.0], "rot": 0.0, "punkte": [[0, 0, 0], [2, 0, 0]]},
|
||
]}
|
||
var bfake := ProbeMain.new()
|
||
bfake.doc = doc
|
||
bfake.history.begin(bfake.doc)
|
||
var bbuilder := ProbeBuilder.new()
|
||
root.add_child(bbuilder)
|
||
var btool := Node3D.new()
|
||
btool.set_script(load("res://src/editor/build_tool.gd"))
|
||
root.add_child(btool)
|
||
btool.setup(bfake, Camera3D.new(), bbuilder, null)
|
||
btool.select_index(0)
|
||
btool.nudge_selected(Vector3(0, 0, -2))
|
||
var list: Array = BuildOps.pieces(doc)
|
||
_check(float(list[0]["pos"][2]) == 4.0 and float(list[0]["pos"][0]) == 4.0,
|
||
"Bauen-Pfeil: exakt 2 m, Raster-Ausrichtung bleibt")
|
||
_check(bfake.history.can_undo(), "Bauen-Pfeil: EIN History-Eintrag")
|
||
bfake.history.undo()
|
||
_check(float(list[0]["pos"][2]) == 6.0, "Bauen-Pfeil: Strg+Z zurück")
|
||
# Strg+Bild↑: Bauteil auf Terrain-Höhe (flaches Terrain → 0 m, 5-cm-Raster).
|
||
list[0]["pos"] = [4.0, 2.5, 6.0]
|
||
btool.snap_selected_to_floor()
|
||
_check(absf(float(list[0]["pos"][1])) < 0.0001, "Bauen Strg+Bild↑: Bauteil auf dem Boden (0 m)")
|
||
# Erneut: liegt schon am Boden → Meldung, kein Commit.
|
||
var can_undo: bool = bfake.history.can_undo()
|
||
btool.snap_selected_to_floor()
|
||
_check(bfake.history.can_undo() == can_undo, "Bauen Strg+Bild↑ am Boden: kein leerer Commit")
|
||
|
||
|
||
# ------------------------------------------ Terrain-Ziffer 4 = Malen (Fix) ----
|
||
|
||
func _test_brush_paint_mode() -> void:
|
||
print("Testgruppe: Terrain-Pinsel — Modus 3 (Malen) kommt durch set_brush_mode")
|
||
var tool := Node3D.new()
|
||
tool.set_script(load("res://src/editor/terrain_tool.gd"))
|
||
root.add_child(tool)
|
||
tool.call("set_brush_mode", TerrainField.Mode.PAINT)
|
||
_check(int(tool.get("brush_mode")) == TerrainField.Mode.PAINT,
|
||
"set_brush_mode(Malen) bleibt Malen (alter Clamp 0..2 degradierte still zu Glätten)")
|
||
tool.call("set_brush_mode", TerrainField.Mode.RAISE)
|
||
_check(int(tool.get("brush_mode")) == TerrainField.Mode.RAISE, "set_brush_mode(Heben) bleibt Heben")
|
||
tool.call("set_brush_mode", 99)
|
||
_check(int(tool.get("brush_mode")) == TerrainField.Mode.PAINT, "Out-of-Range clampet auf den höchsten Modus") |