scopedvr-map-editor/tests/test_gui_smoke.gd
Hermes godot-vr b85e3bed52 Map-Editor: Höhenlinien nur im Terrain-Werkzeug (t_89c0ec95)
Sly-Wunsch 17.10.: "die Dauersicht stört" — die Konturen sind genial, aber
nur im Terrain-Werkzeug (F1) gewünscht.

Kopplung am Werkzeug-Wechselpunkt: _set_tool_mode, _after_structure_change
und _recheck_terrain_mode eichen die Konturen NACH (effektiv = Nutzer-Fix
ODER Terrain-Werkzeug). Kein _process-Poll — die Umschaltung passiert
synchron im selben Aufruf, hart ohne Fade (billig und deterministisch).

NEU: Taste L (Action editor_contours_toggle; L war sonst unbesetzt —
Kollisions-Prüfung im Test) und der Button „≈ Höhenlinien" fixieren die
Linien als NUTZER-Wahl dauerhaft in jedem Werkzeug. Default = Fix AUS,
Linien erscheinen also nur bei F1.

Regression aus dem Fundus gefixt: _after_structure_change baute
terrain_view neu OHNE Kontur-Nacheich (das Gitter hatte ihn, die Linien
nicht) — nach jedem Strg+Z kehrte die Dauersicht zurück. Der Rebuild
bekommt jetzt denselben Nacheich wie das Gitter.

Das Relief-Shading (Hillshade + Höhen-Kurve) bleibt davon unberührt und
immer an. Auf flachen Maps (< 2 m Relief) schaltet der Shader weiter
selbst auf aus.

Doku: docs/tasten.md (L-Zeile in Werkzeuge + Kopplungs-Absatz) und
H-Overlay-Zeile (help_text.gd).

Tests: test_shortcuts +5 Checks (Action registriert, physische Taste L,
Kollisions-Freiheit, L ≠ F1/H). test_gui_smoke +21 Checks am ECHTEN main:
Start AUS · Undo/Rebuild bleibt AUS (Root-Cause-Regression) · F1 ein →
F2/F3/F4 aus (synchron, 0 Frames Poll) · L-Fix an/aus inkl. Button-Sync
und Statusmeldungen · Button-Weg ditto.
Suite 11/11 ALL OK: ux 137, shortcuts 203, editor_svmap 31, terrain 112,
library 94, glb_view 28, spawns 52, testrun 14, bauen 87, gui_smoke 89,
rmb_vorrang 32.
2026-10-17 18:24:45 +00:00

403 lines
19 KiB
GDScript
Raw 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")) == "bauen", "F3: Bauen-Werkzeug aktiv")
_check(str(main.tool_label.text).contains("Bauen"), "Statuszeile: Werkzeug-Label zeigt Bauen")
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 (echter Flow existiert — hier läuft er in den
# „Client nicht gefunden“-Zweig, Hauptsache die Route ist verdrahtet) -----
main._unhandled_key_input(_press_key(KEY_F5, false))
_check(str(main.status_label.text).contains("Karte testen"),
"F5: Karte-testen-Flow angesprungen (Statuszeile meldet)")
_check(main.error_dialog.visible, "F5: Fehler-Dialog im nicht-gefunden-Zweig geöffnet")
main.error_dialog.hide()
# --- 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)")