scopedvr-map-editor/tests/test_gui_smoke.gd
slyphax a76c3ecae7 Bauen-Modus neu: Bauteile als normale Objekte (bau_*), Snap an Kante/Zelle/Ebene, Gebäude = Gruppe
- BauKatalog: 12 Teile (Fundament, Boden, Wände mit Tür/Fenster/Scharte, Brüstung, Pfeiler, Giebel, Dächer, Treppe) auf Basis der CC0-Geometrie des Modular Builder 0.2.0, Wandhöhe 3 m
- BuildSnap: Wände auf Zellkanten, Böden in Zellmitte, Höhe aus Ebenen statt Klickhöhe, Ersetzen (Tür in Wand)
- Gebäude kopieren/auswählen/löschen im Bauen-Tab, Kopien bekommen eigene Gruppe
- Kollision serverkonsistent über Trimesh-Dateien (Türen/Fenster echte Löcher), tools/gen_bauteile.gd erzeugt GLB, Kollision, Registry-Fragment
- Alte gebaeude-Sektion wird beim Laden migriert, altes Bauen-Werkzeug entfernt, Resize behält Gruppennamen
- Tests: test_bauen (258 Checks), Playtest läuft Treppe/Tür

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 16:30:15 +01:00

408 lines
20 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends SceneTree
## Editor-Test (headless): GUI-Smoke — Karte t_d3b93793.
## Ausführen: godot --headless --path . --script res://tests/test_gui_smoke.gd
## Exit 0 = alles grün.
##
## ANDERS als die übrigen Tests instanziert dieser Test die ECHTE Main-UI
## (scenes/main.tscn → src/ui/main.gd) und deckt damit Runtime-UI-Pfade ab,
## die die logik-only-Suite nie sieht (GUI-Start-Befund 15.10.: Terrain-Preset-
## Crash beim Panel-Bau + `command_pressed`-Spam bei jeder Taste — beides
## headless unsichtbar, weil das UI nie instanziert wurde).
##
## Was hier geprüft wird:
## 1. Panel-Bau überlebt _ready (kein abgebrochener _build_right_panel).
## 2. Terrain-Größen-Presets sind initial korrekt belegt (Mittel = 12 m)
## und klickbar (Klick stellt Radius-Slider + Label um).
## 3. Keyboard-Shortcut-Pfad (_unhandled_key_input) läuft ohne
## Property-Fehler und setzt Strg+Z wirklich in Undo um.
var failures := 0
var checks := 0
var main: Control
func _initialize() -> void:
_run_all.call_deferred()
func _run_all() -> void:
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)
# Zwei Frames: _ready + Layout laufen lassen (auch deferred UI-Arbeit).
await process_frame
await process_frame
_test_ui_built()
_test_terrain_presets()
_test_shortcut_undo()
_test_terrain_hotkeys()
_test_shortcuts_karte3()
_test_contours_tool_coupling()
_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)
## Abnahme 1: Der komplette UI-Bau (_ready → _build_ui → _build_right_panel)
## läuft durch — genau dort crashte der Terrain-Preset-Handler (main.gd:659).
func _test_ui_built() -> void:
print("Testgruppe: UI-Bau überlebt _ready")
_check(main != null, "Main-Instanz existiert")
_check(main.terrain_size_btn_small != null, "Preset-Button Klein gebaut")
_check(main.terrain_size_btn_medium != null, "Preset-Button Mittel gebaut")
_check(main.terrain_size_btn_large != null, "Preset-Button Groß gebaut")
_check(main.terrain_radius_slider != null, "Radius-Slider gebaut")
_check(main.undo_btn != null and main.redo_btn != null,
"Undo/Redo-Buttons gebaut (Panel-Bau lief komplett durch)")
## Abnahme 2a: Initialzustand ohne Nebeneffekt-Callback (set_pressed_no_signal):
## Mittel aktiv, Slider passend 12 m.
func _test_terrain_presets() -> void:
print("Testgruppe: Terrain-Größen-Presets")
_check(main.terrain_size_btn_medium.button_pressed,
"Mittel ist initial aktiv")
_check(is_equal_approx(float(main.terrain_radius_slider.value), 12.0),
"Slider startet bei 12 m (passend zum aktiven Preset)")
# Klick auf Groß: button_pressed=true feuert toggled → Handler muss
# jetzt (mit existierendem Slider) Radius + Label setzen.
main.terrain_size_btn_large.button_pressed = true
_check(is_equal_approx(float(main.terrain_radius_slider.value), 32.0),
"Klick auf Groß setzt Slider auf 32 m")
_check(str(main.terrain_radius_label.text).contains("32"),
"Pinsel-Label zeigt 32 m")
_check(main.terrain_size_btn_medium.button_pressed == false,
"ButtonGroup: Mittel beim Klick auf Groß deaktiviert")
# Klick auf Klein (Position-Klick simuliert echten User-Klick).
main.terrain_size_btn_small.button_pressed = true
_check(is_equal_approx(float(main.terrain_radius_slider.value), 4.0),
"Klick auf Klein setzt Slider auf 4 m")
func _press_key(keycode: Key, ctrl: bool, shift := false) -> InputEventKey:
var ev := InputEventKey.new()
ev.keycode = keycode
ev.physical_keycode = keycode
ev.pressed = true
ev.ctrl_pressed = ctrl
ev.shift_pressed = shift
return ev
## Abnahme 2b + 3: Der Shortcut-Pfad muss ohne Property-Fehler durchlaufen
## (vorher crashte `event.command_pressed` bei JEDER Taste) und Strg+Z
## tatsächlich in ein Undo übersetzen.
func _test_shortcut_undo() -> void:
print("Testgruppe: Keyboard-Shortcuts (Strg+Z)")
# Echte History-Aktion in die laufende Editor-History legen.
var before: Dictionary = main.history.snapshot(["objects"])
(main.doc["objects"] as Array).append(
{"id": "test_objekt", "pos": [10.0, 0.0, 10.0], "scale": 1.0,
"rot_y_deg": 0.0})
main.history.commit("Test: Objekt platzieren", before,
main.history.snapshot(["objects"]))
_check((main.doc["objects"] as Array).size() == 1, "Test-Objekt platziert")
# Karte 1 (t_c46d31c2) hat F belegt: F zentriert die Auswahl — ohne
# Auswahl gibt es den freundlichen Hinweis (kein Crash, kein Wirken).
main._unhandled_key_input(_press_key(KEY_F, false))
_check(str(main.status_label.text).contains("Nichts ausgewählt"),
"F ohne Auswahl: freundlicher Hinweis, läuft sauber durch")
main._unhandled_key_input(_press_key(KEY_Z, true))
_check((main.doc["objects"] as Array).is_empty(), "Strg+Z hat das Objekt entfernt (Undo wirkt)")
_check(str(main.status_label.text).contains("Rückgängig gemacht"),
"Statusleiste meldet das Undo")
main._unhandled_key_input(_press_key(KEY_Y, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Y stellt wieder her (Redo wirkt)")
# Leere History + Strg+Z: freundliche Meldung statt Crash.
# Erst das Redo-Ergebnis wieder weg-undoen, DANN ist die History leer.
main._unhandled_key_input(_press_key(KEY_Z, true))
_check((main.doc["objects"] as Array).is_empty(), "Strg+Z nach Redo: History leer")
main._unhandled_key_input(_press_key(KEY_Z, true))
_check(str(main.status_label.text).contains("Nichts zum Rückgängigmachen"),
"Strg+Z bei leerer History: Statusmeldung, kein Absturz")
## Karte 2/3 (t_55257792): Terrain-Ziffern 1–4 + [ / ] am ECHTEN main —
## Buttons/Slider/Label müssen mitwandern, Ziffern außerhalb des Terrain-
## Werkzeugs dürfen NICHTS tun (Konfliktfreiheit).
func _test_terrain_hotkeys() -> void:
print("Testgruppe: Terrain-Pinsel per Tastatur (1–4, [ und ])")
main._set_tool_mode("terrain")
_check(str(main.get("tool_mode")) == "terrain", "Terrain-Modus aktiv")
# Ziffer 2 = Senken: Modus-Button + Pinsel-Modus wandern mit.
main._unhandled_key_input(_press_key(KEY_2, false))
_check(main.terrain_mode_lower.button_pressed, "Ziffer 2: Button „Senken“ aktiv")
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.LOWER,
"Ziffer 2: Pinsel-Modus = Senken")
# Ziffer 4 = Malen: Palette sichtbar, Modus wirklich PAINT (alter
# set_brush_mode-Clamp hätte das still zu Glätten degradiert).
main._unhandled_key_input(_press_key(KEY_4, false))
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.PAINT,
"Ziffer 4: Pinsel-Modus = Malen (Clamp-Fix)")
_check(main.terrain_palette.visible, "Ziffer 4: Material-Palette sichtbar")
# Ziffer 1 zurück: Palette verschwindet wieder.
main._unhandled_key_input(_press_key(KEY_1, false))
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.RAISE,
"Ziffer 1: Pinsel-Modus = Heben")
_check(not main.terrain_palette.visible, "Ziffer 1: Material-Palette weg")
# [ / ] : Preset-Zyklus Klein(4)/Mittel(12)/Groß(32), Slider + Label mit.
# Slider steht nach dem Klein-Klick oben noch auf 4 m — die Kette startet
# also von Klein: ] ] ] = 12 → 32 → wrap 4, [ = 32, [ [ = 12 → 4.
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 12.0),
"] ab 4 m: Slider auf 12 m (Mittel)")
_check(str(main.terrain_radius_label.text).contains("12"), "]: Label zeigt 12 m")
_check(main.terrain_size_btn_medium.button_pressed, "]: Preset-Button Mittel aktiv")
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 32.0),
"] ab 12 m: Slider auf 32 m (Groß)")
_check(str(main.terrain_radius_label.text).contains("32"), "]: Label zeigt 32 m")
_check(main.terrain_size_btn_large.button_pressed, "]: Preset-Button Groß aktiv")
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 4.0),
"] ab 32 m: Zyklus wrappt auf Klein (4 m)")
main._unhandled_key_input(_press_key(KEY_BRACKETLEFT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 32.0),
"[ ab 4 m: wrappt zurück auf Groß (32 m)")
main._unhandled_key_input(_press_key(KEY_BRACKETLEFT, false))
main._unhandled_key_input(_press_key(KEY_BRACKETLEFT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 4.0),
"[ zweimal ab 32 m: 12 → 4 (Klein)")
# Konfliktfreiheit: außerhalb des Terrain-Werkzeugs tun Ziffern/[ NICHTS.
main._set_tool_mode("objects")
var slider_vorher: float = main.terrain_radius_slider.value
main._unhandled_key_input(_press_key(KEY_2, false))
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), slider_vorher),
"Ziffern/[ im Objekt-Modus: Pinsel bleibt unangetastet")
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.RAISE,
"Ziffern im Objekt-Modus: Pinsel-Modus bleibt unangetastet")
## Karte 3/3 (t_11b5c25b): F1–F4 Werkzeugwechsel, F5 Test, H-Hilfe,
## Strg+O/N/S + Dirty-Dialog am ECHTEN main (Dialoge als --mock: sichtbar
## prüfen statt bedienen — headless gibt es keine echte Maus).
func _test_shortcuts_karte3() -> void:
print("Testgruppe: Karte 3/3 — F-Tasten, Hilfe, Datei, Dirty-Dialog")
# --- F1–F4: Werkzeugwechsel (Slys Weiche: F-Tasten statt Ziffern) --------
main._set_tool_mode("objects")
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "terrain", "F1: Terrain-Werkzeug aktiv")
_check(str(main.tool_label.text).contains("Terrain"), "Statuszeile: Werkzeug-Label zeigt Terrain")
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(str(main.get("tool_mode")) == "objects", "F2: zurück zum Objekt-Werkzeug")
main._unhandled_key_input(_press_key(KEY_F3, false))
_check(str(main.get("tool_mode")) == "objects" and (main.ui_right_panel as TabContainer).current_tab == main.bauen_mod.get_index(), "F3: Bauen-Tab geöffnet")
main._unhandled_key_input(_press_key(KEY_F4, false))
_check(str(main.get("tool_mode")) == "spawns", "F4: Spawn-Werkzeug aktiv")
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(str(main.get("tool_mode")) == "objects", "F2 aus dem Spawn-Werkzeug: sauber zurück")
# --- Fokus-Regel: Textfeld fokussiert → F-Tasten wirken NICHT ------------
main.prop_name.grab_focus()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "objects",
"F1 bei fokussiertem Textfeld: Werkzeug bleibt (Tippen geht vor)")
main.prop_name.release_focus()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "terrain", "F1 nach Release-Focus: wirkt wieder")
main._set_tool_mode("objects")
# --- Dialog-Regel: offener Dialog → F-Tasten wirken NICHT ----------------
main.error_dialog.popup_centered()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "objects",
"F1 bei offenem Dialog: Werkzeug bleibt")
main.error_dialog.hide()
# --- H: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls) ----------------
main._unhandled_key_input(_press_key(KEY_H, false))
_check(main.help_overlay.visible, "H: Kurzhilfe-Overlay sichtbar")
var help_txt: String = main.help_overlay_text.text
_check(help_txt.contains("Strg+Z") and help_txt.contains("F1") and help_txt.contains("Terrain"),
"Overlay enthält die Tastenliste aus docs/tasten.md")
_check(str(main.status_label.text).contains("Kurzhilfe an"), "Statuszeile meldet Overlay an")
main._unhandled_key_input(_press_key(KEY_ESCAPE, false))
_check(not main.help_overlay.visible, "Esc schließt das Overlay")
_check(str(main.get("tool_mode")) == "objects", "Esc am Overlay: Objekt-Werkzeug unangetastet")
main._unhandled_key_input(_press_key(KEY_H, false))
main._unhandled_key_input(_press_key(KEY_H, false))
_check(not main.help_overlay.visible, "H zweimal: Overlay wieder zu (Toggle)")
# --- F5: Karte testen (Karte t_1cdc1a72): startet den Lauf-Modus im
# Builder selbst — kein Client, kein Fehler-Dialog. ESC verlässt ihn
# sauber (Panels wieder sichtbar), danach geht der Test weiter. ---------
main._unhandled_key_input(_press_key(KEY_F5, false))
_check(str(main.status_label.text).contains("Testlauf läuft"),
"F5: Lauf-Modus gestartet (Statuszeile meldet)")
_check(not main.error_dialog.visible,
"F5: kein Fehler-Dialog (Lauf-Modus braucht keinen Client)")
_check(main.get("playtest") != null, "F5: PlaytestMode ist aktiv")
main._unhandled_key_input(_press_key(KEY_ESCAPE, false))
_check(main.get("playtest") == null, "F5+ESC: Lauf-Modus sauber beendet")
_check(main.error_dialog == null or not main.error_dialog.visible,
"F5+ESC: kein Fehler-Dialog offen")
# --- Dirty-Schutz: Strg+N bei ungespeicherten Änderungen FRAGT ZUERST ----
main.dirty = true
main._unhandled_key_input(_press_key(KEY_N, true))
_check(main.dirty_dialog.visible, "Strg+N bei dirty: Bestätigungs-Dialog ZUERST")
_check(not main.new_dialog.visible, "Strg+N bei dirty: Neue-Map-Dialog wartet hinterher")
_check(str(main.dirty_dialog.dialog_text).contains("ungespeicherte Änderungen"),
"Dirty-Dialog: deutsche Klartext-Meldung")
# „Verwerfen“ (confirmed) → jetzt erst geht der Neue-Map-Dialog auf.
main.dirty_dialog.confirmed.emit()
_check(main.new_dialog.visible, "Verwerfen bestätigt: Neue-Map-Dialog öffnet jetzt")
main.new_dialog.hide()
main.dirty = false
# --- Strg+O ohne dirty: Öffnen-Dialog DIREKT -----------------------------
main._unhandled_key_input(_press_key(KEY_O, true))
_check(main.open_dialog.visible, "Strg+N/Strg+O ohne dirty: Öffnen-Dialog direkt")
main.open_dialog.hide()
# --- Strg+S: speichert an bekannten Pfad, dirty wird aufgeräumt ----------
main.current_path = "/tmp/svmap_karte3_test.svmap"
main.dirty = true
main._unhandled_key_input(_press_key(KEY_S, true))
_check(FileAccess.file_exists("/tmp/svmap_karte3_test.svmap"), "Strg+S: .svmap geschrieben")
_check(not main.dirty, "Strg+S: dirty zurückgesetzt")
_check(str(main.status_label.text).contains("gespeichert"), "Strg+S: Statuszeile meldet Speichern")
# --- Strg+Umschalt+S: Speichern-unter-Dialog ------------------------------
main._unhandled_key_input(_press_key(KEY_S, true, true))
_check(main.save_dialog.visible, "Strg+Umschalt+S: Speichern-unter-Dialog")
main.save_dialog.hide()
# --- Strg+Umschalt+Z: der zweite Redo-Weg --------------------------------
var before: Dictionary = main.history.snapshot(["objects"])
(main.doc["objects"] as Array).append(
{"id": "test_redo_objekt", "pos": [4.0, 0.0, 4.0], "scale": 1.0,
"rot_y_deg": 0.0})
main.history.commit("Test: Redo-Objekt", before, main.history.snapshot(["objects"]))
main._unhandled_key_input(_press_key(KEY_Z, true))
_check((main.doc["objects"] as Array).size() == 0, "Strg+Z: Objekt weg (Undo)")
main._unhandled_key_input(_press_key(KEY_Z, true, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Umschalt+Z: Redo-Weg 2 stellt her")
main._unhandled_key_input(_press_key(KEY_Z, true))
main._unhandled_key_input(_press_key(KEY_Y, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Y: Redo-Weg 1 stellt auch her")
# Aufräumen: Undo, damit spätere Gruppen (falls Testreihenfolge sich ändert)
# nicht von diesem Objekt ausgehen.
main._unhandled_key_input(_press_key(KEY_Z, true))
## Karte t_89c0ec95: Höhenlinien NUR im Terrain-Werkzeug — Kopplung am
## Werkzeugwechsel (F1 ein, F2/F3/F4 aus), Fix über L / Button. Der Test
## drückt alles SYNCHRON durch _set_tool_mode bzw. _unhandled_key_input:
## die Sichtbarkeit kippt im selben Aufruf — der Beweis, dass kein
## _process-Poll nötig ist (0 Frames).
func _test_contours_tool_coupling() -> void:
print("Testgruppe: Höhenlinien-Werkzeug-Kopplung (t_89c0ec95)")
# Aufräumen aus der karte3-Gruppe: programmisches confirmed.emit() schließt
# das dirty-Dialog-Window NICHT — ein sichtbares Window sperrt das
# F/L-Tasten-Gate (ShortcutPolicy). Deshalb hier alle Windows zu.
for child in main.get_children():
if child is Window:
child.hide()
main._set_tool_mode("objects")
var terrain_view = main.view_builder.get("terrain_view")
_check(terrain_view != null, "Terrain-View existiert im Builder")
_check(str(main.get("tool_mode")) == "objects", "Start: Objekt-Werkzeug")
_check(not bool(terrain_view.get("_contours_pref")),
"Start: Höhenlinien AUS (kein Fix, kein Terrain-Werkzeug)")
# Regression zur obigen Root-Cause: Struktur-Änderung (Undo) baut das
# Terrain-Mesh NEU — die Linien dürfen NICHT auf Default „an“ zurück-
# springen (vor t_89c0ec95 geschah genau das nach jedem Strg+Z).
var before_obj: Dictionary = main.history.snapshot(["objects"])
(main.doc["objects"] as Array).append(
{"id": "test_contour_objekt", "pos": [2.0, 0.0, 2.0], "scale": 1.0,
"rot_y_deg": 0.0})
main.history.commit("Test: Kontur-Objekt", before_obj,
main.history.snapshot(["objects"]))
main._do_undo()
terrain_view = main.view_builder.get("terrain_view") # Rebuild → neue View
_check(terrain_view != null, "Undo: Terrain-View frisch vorhanden")
_check(not bool(terrain_view.get("_contours_pref")),
"Struktur-Änderung (Undo + Rebuild): Höhenlinien bleiben AUS im Objekt-Werkzeug")
# F1 → Terrain: Linien EIN, synchron im Wechselpunkt.
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "terrain", "F1: Terrain-Werkzeug an")
_check(bool(terrain_view.get("_contours_pref")),
"F1: Höhenlinien EIN (synchron, 0 Frames Poll)")
# Jedes andere Werkzeug: Linien wieder AUS.
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(str(main.get("tool_mode")) == "objects", "F2: zurück zum Objekt-Werkzeug")
_check(not bool(terrain_view.get("_contours_pref")), "F2: Höhenlinien AUS")
main._unhandled_key_input(_press_key(KEY_F3, false))
_check(not bool(terrain_view.get("_contours_pref")), "F3 (Bauen): Höhenlinien AUS")
main._unhandled_key_input(_press_key(KEY_F4, false))
_check(not bool(terrain_view.get("_contours_pref")), "F4 (Spawns): Höhenlinien AUS")
main._unhandled_key_input(_press_key(KEY_F2, false))
# L = Fixieren: sichtbar sogar AUSSERHALB des Terrain-Werkzeugs.
main._unhandled_key_input(_press_key(KEY_L, false))
_check(bool(terrain_view.get("_contours_pref")),
"L: Höhenlinien FIXIERT AN (sichtbar im Objekt-Werkzeug)")
_check(main.contours_btn.button_pressed,
"L: Button „≈ Höhenlinien“ mitgesynced (pressed)")
_check(str(main.status_label.text).contains("FIXIERT"),
"L: Statuszeile meldet Fix AN")
# Fix bleibt über Werkzeugwechsel bestehen …
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(bool(terrain_view.get("_contours_pref")), "Fix an: F1 lässt Linien an")
# … aber ein Werkzeugwechsel KANN bei Fix-an nie ausblenden. L im Terrain-
# Werkzeug löst den Fix: Linien bleiben (Werkzeug-Kopplung trägt weiter).
main._unhandled_key_input(_press_key(KEY_L, false))
_check(not bool(main.get("_contours_on")), "L im Terrain: Fix gelöst")
_check(bool(terrain_view.get("_contours_pref")),
"L im Terrain: Linien bleiben AN (Werkzeug-Kopplung trägt)")
_check(not main.contours_btn.button_pressed,
"L im Terrain: Button-Fix mitgesynced (unpressed)")
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(not bool(terrain_view.get("_contours_pref")),
"F2 nach Fix-Lösung: Höhenlinien wieder AUS")
# Button-Weg (ohne L): Drücken des Buttons fixiert ditto.
main.contours_btn.set_pressed(true) # feuert toggled → _on_contours_toggled
_check(bool(terrain_view.get("_contours_pref")),
"Button: Fix AN fixiert Linien im Objekt-Werkzeug")
main.contours_btn.set_pressed(false)
_check(not bool(terrain_view.get("_contours_pref")),
"Button: Fix AUS blendet zurück (nur Terrain-Werkzeug)")