From 7521db7f8d8b0d7693235e8a9487e94f167a080d Mon Sep 17 00:00:00 2001 From: "Hermes (godot-vr)" Date: Sat, 17 Oct 2026 17:36:57 +0000 Subject: [PATCH] =?UTF-8?q?Map-Editor:=20RMB=20=3D=20Flug=20mit=20absolute?= =?UTF-8?q?m=20Vorrang=20(t=5F2470de89)=20=E2=80=94=20Terrain-Senken=20auf?= =?UTF-8?q?=20Strg+LMB,=20Hand-Objekt-Abwurf=20auf=20Esc.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Terrain-Pinsel: Rechtsklick startet keinen Strich mehr; Strg+LMB invertiert (Heben↔Senken, Blender/Photoshop-Konvention; Malen: Strg+Klick = Gras zurück). - Platzieren: RMB bricht nicht mehr ab — Objekt bleibt in der Hand, Abwurf nur über Esc. build_tool/spawn_tool: RMB-Deselect/Werkzeugwechsel entfernt (Auswahl-Deselect = Esc / LMB auf Leere). - orbit_camera: rmb_filter entfernt — Flug startet in JEDEM Werkzeugzustand. - docs/tasten.md + H-Overlay + Status-/Paneltexte aktualisiert. - Neuer Test tests/test_rmb_vorrang.gd (32 Checks, echte Main-UI headless); in tools/run_tests.sh aufgenommen. Suite grün (test_glb_view: 12/22 FAIL auch auf origin/main — fehlende GLB-Quellen im Scratch-Workspace, umgebungsbedingt). --- README.md | 9 +- docs/tasten.md | 5 +- src/editor/build_tool.gd | 16 +- src/editor/interaction.gd | 14 +- src/editor/spawn_tool.gd | 13 +- src/editor/terrain_tool.gd | 41 ++--- src/ui/help_text.gd | 4 +- src/ui/main.gd | 23 ++- src/viewport/orbit_camera.gd | 12 +- tests/test_rmb_vorrang.gd | 309 ++++++++++++++++++++++++++++++++++ tests/test_rmb_vorrang.gd.uid | 1 + tools/run_tests.sh | 2 +- 12 files changed, 376 insertions(+), 73 deletions(-) create mode 100644 tests/test_rmb_vorrang.gd create mode 100644 tests/test_rmb_vorrang.gd.uid diff --git a/README.md b/README.md index 5e7ea6f..df9acb4 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ Boden/Wetter-Stimmung — siehe Commit 84bc081 und `svmap_io.gd`.)* - **Objekt-Platzierung mit Ghost-Preview (`src/editor/interaction.gd`):** Bibliothek links (deutsche Namen + Poly-Zahl), Klick startet den Platzier-Modus — eine transparente Vorschau schwebt am Cursor auf dem Boden - (Höhe kommt aus dem Terrain), Klick setzt, Rechtsklick/ESC bricht ab, + (Höhe kommt aus dem Terrain), Klick setzt, ESC bricht ab (Rechte + Maustaste = Flug, das Objekt bleibt in der Hand — Karte t_2470de89), Mausrad dreht die Vorschau VOR dem Setzen (15°-Raster). Modus bleibt an für schnelles Mehrfachsetzen. Poly-Budget wird VOR dem Platzieren geprüft. - **Auswahl-Anfassen:** Klick wählt ein Objekt (orange + Emission), Ziehen @@ -309,8 +310,10 @@ Shortcut-Karten — Karten 2/3 ergänzen dort. beim Flug-Exit werden Ziel/Winkel/Abstand aus Flugposition/Blickrichtung zurückgerechnet (kein Kamera-Sprung, getestet). Mathe pur in `src/editor/fly_math.gd`, Kamera dünn (`src/viewport/orbit_camera.gd`). - Konflikt-Auflösung: RMT beim Platzieren gehört dem Ghost-Abbruch - (`rmb_filter`), Mausrad im Flug gehört dem Tempo (interaction blockt). + Konflikt-Auflösung: RMB hat ABSOLUTEN Vorrang für den Flug — in JEDEM + Werkzeugzustand (Terrain, Bauen, Spawn, Platzieren; Karte t_2470de89, + kein `rmb_filter` mehr), Mausrad im Flug gehört dem Tempo (interaction + blockt). - **F** = Auswahl ins Bild zentrieren — Orbit-Ziel wandert auf den Auswahl- Mittelpunkt (Anker + halbe Mesh-Höhe, `SelectionMath.focus_center`); im Flug dreht sich nur die Blickrichtung. diff --git a/docs/tasten.md b/docs/tasten.md index 618e0ad..d23bce1 100644 --- a/docs/tasten.md +++ b/docs/tasten.md @@ -27,7 +27,7 @@ Konventionen (alle Karten): | Mittlere Maustaste ziehen | Orbit: Um die Karte drehen | | Mittlere Maustaste + Umschalt ziehen | Pan: Karte verschieben | | Mausrad | Zoom (aus, wenn Mausrad gerade ein Objekt dreht) | -| **Rechte Maustaste GEDACHT halten** | **Freelook-Flug:** Umsehen + W vor / S zurück / A links / D rechts; E hoch / Q runter | +| **Rechte Maustaste GEDACHT halten** | **Freelook-Flug:** Umsehen + W vor / S zurück / A links / D rechts; E hoch / Q runter. **Funktioniert in JEDEM Werkzeug** (Terrain, Bauen, Spawn-Zonen, Platzieren) — die rechte Maustaste hat ABSOLUTEN Vorrang, kein Werkzeug belegt sie (Karte t_2470de89) | | Umschalt (im Flug) | schneller fliegen (×3) | | Alt (im Flug) | langsamer fliegen (×0,3) | | Mausrad (im Flug) | Grundgeschwindigkeit dauerhaft ändern (2–300 m/s) | @@ -91,6 +91,9 @@ nichts geht versehentlich verloren. | **2** | Pinselmodus Senken | | **3** | Pinselmodus Glätten | | **4** | Pinselmodus Malen (mit der zuletzt gewählten Materialfarbe) | +| **Linksklick-Ziehen** | Boden formen bzw. malen (je nach Pinselmodus) | +| **Strg + Linksklick-Ziehen** | **Gegenteil:** bei Heben senkt er, bei Senken hebt er (Invert-Konvention wie Blender/Photoshop); im Malen-Modus malt Strg+Klick Gras zurück | +| Rechte Maustaste | **Kein Terrain-Strich mehr** — gehört dem Freelook-Flug (absoluter Vorrang, siehe Navigation) | | Strg halten | temporärer Snap-Zwang beim Objekt-Bewegen/Platzieren (gilt editorweit, nicht nur hier) | Konfliktfreiheit: Die Ziffern 1–4 sind NUR im Terrain-Werkzeug belegt — im diff --git a/src/editor/build_tool.gd b/src/editor/build_tool.gd index b0841ae..5486435 100644 --- a/src/editor/build_tool.gd +++ b/src/editor/build_tool.gd @@ -11,7 +11,8 @@ extends Node3D ## Bild↑/Bild↓ → Auswahl anheben/absenken (0,2 m — Wände aufs ## Fundament setzen) ## Entf → Auswahl löschen -## Rechtsklick/ESC → Auswahl weg, nochmal = Werkzeug beenden +## ESC → Auswahl weg, nochmal = Werkzeug beenden +## (Rechte Maustaste = immer Flug, t_2470de89) ## ## UNDO: EIN Rückgängig-Schritt PRO Bauteil (ActionHistory, Block "gebaeude"). @@ -143,14 +144,9 @@ func _unhandled_input(event: InputEvent) -> void: else: _place_piece() get_viewport().set_input_as_handled() - elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: - if selected_index >= 0: - selected_index = -1 - view_builder.call("set_gebaeude_selected", -1) - main.call("_set_status", "Auswahl aufgehoben — Linksklick setzt weiter Bauteile.") - else: - main.call("_set_tool_mode", "objects") - get_viewport().set_input_as_handled() + # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) — + # sie gehört mit absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug + # beenden läuft über ESC (main.gd), die Auswahl bleibt im Flug erhalten. func _update_cursor(viewport_pos: Vector2) -> void: @@ -223,7 +219,7 @@ func _select_at(viewport_pos: Vector2) -> void: selected_index = int(pick["piece"]) view_builder.call("set_gebaeude_selected", selected_index) var label := _piece_label(selected_index) - main.call("_set_status", "%s ausgewählt — R drehen · Bild↑/↓ anheben · Entf löschen · Rechtsklick: Auswahl weg." % label) + main.call("_set_status", "%s ausgewählt — R drehen · Bild↑/↓ anheben · Entf löschen · Esc: Auswahl weg." % label) func _piece_label(idx: int) -> String: diff --git a/src/editor/interaction.gd b/src/editor/interaction.gd index ec14037..ccb68e8 100644 --- a/src/editor/interaction.gd +++ b/src/editor/interaction.gd @@ -191,7 +191,7 @@ func start_placement(oid: String) -> void: var label: String = main.object_label(oid) var hinweis := "" if not GlbMeshStore.fehlt(oid) \ else " (Platzhalter — echte GLB fehlt für %s)" % oid - main.call("_set_status", "%s%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, Rechtsklick/ESC bricht ab." % [label, hinweis]) + main.call("_set_status", "%s%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, ESC bricht ab (Rechte Maustaste = Flug)." % [label, hinweis]) main.call("_highlight_library", oid) @@ -314,7 +314,7 @@ func _place_object(viewport_pos: Vector2) -> void: main.call("_after_structure_change") 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/Rechtsklick beendet. + # Auswahl auf das frische Objekt legen? Nein — Ghost bleibt Vorrang; ESC beendet. main.call("_mark_tree_selection", new_index) @@ -877,11 +877,9 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: _on_left_release(mb) - elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: - if placing: - cancel_placement() - main.call("_set_status", "Platzieren abgebrochen.") - get_viewport().set_input_as_handled() + # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) — + # sie gehört mit absolutem Vorrang dem Freelook-Flug (orbit_camera.gd). + # Beim Platzieren bleibt das Objekt IN der Hand, Abwurf nur über Esc. elif mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed: if _camera_flying(): return # Mausrad = Flug-Tempo (orbit_camera), kein Objekt-Drehen @@ -1114,7 +1112,7 @@ func demo_park_ghost(world_pos: Vector3, oid: String) -> void: _ghost.visible = true _ghost_active = true main.call("_highlight_library", oid) - main.call("_set_status", "%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, Rechtsklick/ESC bricht ab." % main.object_label(oid)) + main.call("_set_status", "%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, ESC bricht ab (Rechte Maustaste = Flug)." % main.object_label(oid)) ## Demo-Hook (Gruppen-Smoke, Karte t_f6f29d86): die Auswahl EXAKT so ziehen diff --git a/src/editor/spawn_tool.gd b/src/editor/spawn_tool.gd index 2e7a648..d5a08a3 100644 --- a/src/editor/spawn_tool.gd +++ b/src/editor/spawn_tool.gd @@ -7,7 +7,8 @@ extends Node3D ## Linksklick auf Zone = Zone auswählen; ZIEHEN verschiebt sie (Terrain-Höhe) ## Regel rechts = Radius der neuen Zonen; mit Auswahl: Radius DER Zone ## Entf = ausgewählte Zone löschen -## Rechtsklick/ESC = Auswahl weg / Werkzeug beendet (ESC in main.gd) +## ESC = Auswahl weg / Werkzeug beendet (ESC in main.gd; +## Rechte Maustaste = immer Flug, t_2470de89) ## ## Jede Operation ist EIN Undo-Eintrag über die gemeinsame ActionHistory ## (Blöcke spawn_zones + design_notes, SpawnOps.snapshot_blocks). @@ -171,13 +172,9 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: _on_left_release() - elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: - if selected_team >= 0: - _clear_selection() - main.call("_set_status", "Auswahl aufgehoben — klicke auf den Boden, um eine neue Zone zu malen.") - else: - main.call("_set_tool_mode", "objects") - get_viewport().set_input_as_handled() + # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) — + # sie gehört mit absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug + # beenden läuft über ESC (main.gd), die Auswahl bleibt im Flug erhalten. func _update_cursor(viewport_pos: Vector2) -> void: diff --git a/src/editor/terrain_tool.gd b/src/editor/terrain_tool.gd index be9e5a8..c931290 100644 --- a/src/editor/terrain_tool.gd +++ b/src/editor/terrain_tool.gd @@ -4,8 +4,11 @@ extends Node3D ## ## Bedienung: ## Linksklick-Ziehen = gewählter Pinsel-Modus (Heben / Senken / Glätten) -## Rechtsklick-Ziehen = Gegenteil (bei Heben senkt er, bei Senken hebt er — -## kein Umschalten nötig; bei Glätten senkt er) +## Strg+Linksklick = das Gegenteil (bei Heben senkt er, bei Senken hebt +## er — Invert-Konvention wie Blender/Photoshop; im +## Malen-Modus malt Strg+Klick Gras zurück). Karte +## t_2470de89: die RECHTE Maustaste gehört IMMER dem +## Freelook-Flug — sie startet hier keinen Strich mehr. ## Der ganze Strich = EIN Undo-Eintrag (Strg+Z stellt die Form VOR dem ## Strich wieder her, nicht nur eine Zwischenstufe). ## @@ -151,26 +154,26 @@ func _unhandled_input(event: InputEvent) -> void: var mb := event as InputEventMouseButton if mb.button_index == MOUSE_BUTTON_LEFT: if mb.pressed: - _begin_stroke(brush_mode) + # Karte t_2470de89: Strg = Invert-Modifier. Heben↔Senken + # tauschen (Blender/Photoshop-Konvention), im Malen-Modus + # malt Strg+Klick Gras zurück (alter Radierer-Weg). + var mode := brush_mode + var mat := -1 + if mb.ctrl_pressed: + match brush_mode: + TerrainField.Mode.RAISE: + mode = TerrainField.Mode.LOWER + TerrainField.Mode.LOWER: + mode = TerrainField.Mode.RAISE + TerrainField.Mode.PAINT: + mat = 0 # Gras zurückmalen + _begin_stroke(mode, mat) else: _end_stroke() get_viewport().set_input_as_handled() - elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: - # Rechtsklick = das Gegenteil zum Modell: bei Heben senken, - # bei Senken heben, bei Glätten senken (kein Umschalten nötig). - # Im Malen-Modus malt Rechtsklick Gras zurück (Radierer-Semantik). - if brush_mode == TerrainField.Mode.PAINT: - _begin_stroke(TerrainField.Mode.PAINT, 0) - else: - var opposite: int = TerrainField.Mode.LOWER - if brush_mode == TerrainField.Mode.LOWER: - opposite = TerrainField.Mode.RAISE - _begin_stroke(opposite) - get_viewport().set_input_as_handled() - elif mb.button_index == MOUSE_BUTTON_RIGHT and not mb.pressed: - if _stroking: - _end_stroke() - get_viewport().set_input_as_handled() + # Rechte Maustaste: absichtlich NICHT behandelt — sie gehört mit + # absolutem Vorrang dem Freelook-Flug (orbit_camera.gd, Karte + # t_2470de89), in jedem Werkzeugzustand. func _update_cursor(viewport_pos: Vector2) -> void: diff --git a/src/ui/help_text.gd b/src/ui/help_text.gd index 92fbd88..ec13af0 100644 --- a/src/ui/help_text.gd +++ b/src/ui/help_text.gd @@ -14,7 +14,7 @@ static func text() -> String: ["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"], ["Mittlere Maustaste + Umschalt ziehen", "Pan: Karte verschieben"], ["Mausrad", "Zoom (aus, während ein Objekt gedreht wird)"], - ["Rechte Maustaste GEDACHT halten", "Freelook-Flug: Umsehen + W vor / S zurück / A links / D rechts; E hoch / Q runter"], + ["Rechte Maustaste GEDACHT halten", "Freelook-Flug: Umsehen + W vor / S zurück / A links / D rechts; E hoch / Q runter — funktioniert in JEDEM Werkzeug (Terrain, Bauen, Platzieren), die rechte Maustaste hat immer Vorrang"], ["Umschalt (im Flug)", "schneller fliegen (×3)"], ["Alt (im Flug)", "langsamer fliegen (×0,3)"], ["Mausrad (im Flug)", "Grundgeschwindigkeit dauerhaft ändern (2–300 m/s)"], @@ -47,7 +47,7 @@ static func text() -> String: ["1 / 2 / 3 / 4", "Pinselmodus: Heben / Senken / Glätten / Malen"], ["[ / ]", "Pinselgröße: Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch"], ["Linksklick-Ziehen", "Boden formen bzw. malen"], - ["Rechtsklick-Ziehen", "Gegenteil formen / Gras zurückmalen"], + ["Strg+Linksklick-Ziehen", "Gegenteil: senken (bei Heben) / heben (bei Senken) / Gras zurückmalen"], ])) out += _header("Datei & Undo") out += _table(_rows([ diff --git a/src/ui/main.gd b/src/ui/main.gd index 1bdf464..bddeae4 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -388,7 +388,7 @@ func _build_left_panel() -> PanelContainer: panel.add_child(vbox) vbox.add_child(_header_label("Objekt-Bibliothek")) - vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis Rechtsklick/ESC.")) + vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis ESC (Rechte Maustaste = Flug).")) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL @@ -503,9 +503,10 @@ func _build_center_viewport() -> SubViewportContainer: world.add_child(interaction) interaction.setup(self, camera, view_builder) camera.set("wheel_filter", Callable(interaction, "wheel_blocked")) - # Freelook-Flug-Verdrahtung (Karte t_c46d31c2): RMT gehört beim Platzieren - # dem Ghost-Abbruch, Status-Meldungen der Kamera laufen ins Statusfeld. - camera.set("rmb_filter", Callable(self, "_fly_rmb_blocked")) + # Freelook-Flug-Verdrahtung (Karte t_c46d31c2): Status-Meldungen der + # Kamera laufen ins Statusfeld. Karte t_2470de89: die rechte Maustaste + # gehört IMMER dem Flug — kein Filter mehr, die Werkzeuge fassen RMB + # nicht mehr an. camera.set("status_cb", Callable(self, "_set_status")) # Terrain-Pinsel (Karte t_9aba04f1) — eigener Eingabe-Knoten im Weltraum. @@ -789,8 +790,8 @@ func _build_right_panel() -> PanelContainer: terrain_panel.add_child(terrain_strength_slider) terrain_panel.add_child(_muted_label( - "Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Rechtsklick-Ziehen macht das Gegenteil — im Malen-Modus malt Rechtsklick Gras zurück. " - + "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Terrain-Modus.")) + "Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Strg+Linksklick macht das Gegenteil — im Malen-Modus malt Strg+Klick Gras zurück. " + + "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). Rechte Maustaste = Flug, ESC beendet den Terrain-Modus.")) # --- Spawn-Zonen (Karte t_c4162c5d): Live-Status IMMER sichtbar --- vbox.add_child(_separator()) @@ -1177,10 +1178,8 @@ func _focus_selection() -> void: _set_status("%d Objekt(e) ins Bild zentriert — Strg+Klick/Strg+LMB markiert weitere dazu, Entf löscht die ganze Auswahl." % indices.size()) -## Rechtsklick-Filter der Kamera: während des Platzierens gehört die rechte -## Maustaste dem Ghost-Abbruch (interaction) — der Flug startet dann nicht. -func _fly_rmb_blocked() -> bool: - return interaction.placing +## Karte t_2470de89: Der alte Rechtsklick-Filter (_fly_rmb_blocked) ist weg — +## RMB startet den Freelook-Flug in JEDEM Werkzeugzustand (absoluter Vorrang). func _refresh_history_buttons() -> void: @@ -1523,7 +1522,7 @@ func _enter_terrain_mode() -> void: return tool_mode = "terrain" terrain_panel.visible = true - help_label.text = "Linksklick-Ziehen: Boden formen oder malen · Rechtsklick-Ziehen: Gegenteil / Gras zurück ·\n1/2/3/4: Heben/Senken/Glätten/Malen · [ / ]: Pinselgröße Klein/Mittel/Groß ·\nPinselgröße & Material rechts · Mittlere Maustaste: Kamera · Strg+Z: letzter Strich weg" + help_label.text = "Linksklick-Ziehen: Boden formen oder malen · Strg+Linksklick: Gegenteil / Gras zurück ·\n1/2/3/4: Heben/Senken/Glätten/Malen · [ / ]: Pinselgröße Klein/Mittel/Groß ·\nPinselgröße & Material rechts · Rechte Maustaste: Flug · Strg+Z: letzter Strich weg" terrain_edit_btn.button_pressed = true terrain_tool.call("set_brush_mode", _selected_terrain_mode()) _set_status("Terrain-Modus an: LINKSKLICK-Ziehen formt den Boden, im Malen-Modus färbt er das Gelände (Gras/Sand/Fels). Regler rechts stellen Pinsel und Stärke ein.") @@ -2296,7 +2295,7 @@ func _start_terrain_demo(args: PackedStringArray) -> void: terrain_tool.call("demo_stroke", 90.0, 50.0, TerrainField.Mode.LOWER, 1.2) terrain_tool.call("demo_park_brush", 90.0, 50.0) _demo_camera(Vector3(90.0, 2.0, 50.0), 150.0, 32.0, -50.0) - _set_status("Demo 2/5: Senke GESSENKT (Rechtsklick-Ziehen senkt, ohne umzuschalten).") + _set_status("Demo 2/5: Senke GESSENKT (Strg+Linksklick senkt, ohne umzuschalten).") await _demo_shot(out_dir + "/terrain_lower.png") # 3) Hügelkuppe glätten (blauer Ring). diff --git a/src/viewport/orbit_camera.gd b/src/viewport/orbit_camera.gd index 56c9133..650d4c1 100644 --- a/src/viewport/orbit_camera.gd +++ b/src/viewport/orbit_camera.gd @@ -30,9 +30,6 @@ var distance := 300.0 ## Von main.gd gesetzt: Callable → true, wenn das Mausrad gerade ein Objekt ## drehen soll (Platzier-Modus oder Auswahl) statt zu zoomen. var wheel_filter := Callable() -## Von main.gd gesetzt: Callable → true, wenn Rechtsklick belegt ist (z. B. -## Platzier-Abbruch) — dann startet der Freelook-Flug nicht. -var rmb_filter := Callable() ## Von main.gd gesetzt: Callable für Klartext-Status-Meldungen. var status_cb := Callable() @@ -73,9 +70,10 @@ func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton: var mb := event as InputEventMouseButton if mb.button_index == MOUSE_BUTTON_RIGHT: + # Karte t_2470de89: RMB = Flug mit ABSOLUTEM Vorrang — kein Filter, + # keine Heuristik. Werkzeuge behandeln RMB nicht mehr, also startet + # der Flug in JEDEM Werkzeugzustand (auch Terrain/Platzieren/Bauen). if mb.pressed: - if _rmb_blocked(): - return _enter_fly() _fly_held = true elif _fly_held: @@ -134,10 +132,6 @@ func _wheel_blocked() -> bool: return wheel_filter.is_valid() and bool(wheel_filter.call()) -func _rmb_blocked() -> bool: - return rmb_filter.is_valid() and bool(rmb_filter.call()) - - func distance_max() -> float: return 6000.0 diff --git a/tests/test_rmb_vorrang.gd b/tests/test_rmb_vorrang.gd new file mode 100644 index 0000000..a3b4913 --- /dev/null +++ b/tests/test_rmb_vorrang.gd @@ -0,0 +1,309 @@ +extends SceneTree +## Editor-Test (headless): Karte t_2470de89 — RMB = Flug mit ABSOLUTEM Vorrang. +## Ausführen: godot4 --headless --path . --script res://tests/test_rmb_vorrang.gd +## Exit 0 = alles grün. +## +## Instanziert die ECHTE Main-UI (wie test_gui_smoke) und schickt echte +## Maus-/Tastatur-Events durch den Viewport. Geprüft wird: +## 1. Terrain-Werkzeug aktiv + rechte Maustaste → Flug startet, KEIN +## Terrain-Strich (Höhenfeld byte-identisch, keine History-Zeile). +## 2. Strg+Linksklick im Terrain-Werkzeug SENKT (bei Pinsel-Modus Heben), +## linker Klick hebt — Invert-Konvention. +## 3. Objekt „in der Hand“ (Platzier-Modus) + rechte Maustaste → Flug, +## das Objekt BLEIBT in der Hand nach dem Flug; Esc wirft ab. +## 4. Bauen-Werkzeug: rechte Maustaste fliegt, wechselt NICHT mehr das +## Werkzeug und wirft die Bauteil-Auswahl nicht weg. + +var failures := 0 +var checks := 0 +var main: Control +var camera: Camera3D +var container: SubViewportContainer + + +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) + await process_frame + await process_frame + + container = _find_viewport_container(main) + _check(container != null, "SubViewportContainer gefunden") + camera = main.get("camera") + _check(camera != null, "Kamera-Referenz gefunden") + if container == null or camera == null: + _finish() + return + subvp = container.get_child(0) as SubViewport + _check(subvp != null, "SubViewport gefunden") + + await _test_terrain_rmb_flies() + await _test_terrain_ctrl_lmb() + await _test_placement_rmb_keeps_hand() + await _test_build_rmb_flies() + + _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 + + +## Headless-Besonderheit: das Fenster ist nur 64×64 groß — root.push_input +## leitet NICHT in den SubViewportContainer weiter (GUI-Delivery braucht das +## Event im Fenster-Rect). Deshalb pushen wir direkt in den SubViewport mit +## LOKALEN Koordinaten — die Werkzeug-/Kamera-Handler laufen exakt wie echt. +var subvp: SubViewport + + +## Event-Position: Mitte des 3D-Viewports (in SubViewport-Koordinaten). +func _vp_center() -> Vector2: + return subvp.size * 0.5 + + +func _mouse(button: MouseButton, pressed: bool, pos: Vector2, ctrl := false, shift := false) -> void: + var mb := InputEventMouseButton.new() + mb.button_index = button + mb.pressed = pressed + mb.position = pos + mb.global_position = pos + mb.ctrl_pressed = ctrl + mb.shift_pressed = shift + subvp.push_input(mb) + + +func _motion(pos: Vector2) -> void: + var mm := InputEventMouseMotion.new() + mm.position = pos + mm.global_position = pos + mm.relative = Vector2.ZERO + 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 _heights() -> PackedFloat32Array: + return SvmapIO.decode_heights((main.doc.get("terrain", {}) as Dictionary)) + + +func _height_at(heights: PackedFloat32Array, x: float, z: float) -> float: + var terrain: Dictionary = main.doc.get("terrain", {}) + return TerrainField.height_at(heights, + int(terrain.get("resolution", 0)), + float(terrain.get("size_x_m", 0.0)), + float(terrain.get("size_z_m", 0.0)), x, z) + + +## Abnahme 1: Terrain-Werkzeug aktiv + RMB → Flug an, KEIN Terrain-Change, +## RMB loslassen → Flug aus. Auch im Flug bewegt die Kamera mit W. +func _test_terrain_rmb_flies() -> void: + print("Testgruppe: Terrain-Werkzeug + RMB = Flug (kein Strich)") + main.call("_set_tool_mode", "terrain") + _check(str(main.get("tool_mode")) == "terrain", "Terrain-Werkzeug aktiv") + var before := _heights() + var pos := _vp_center() + _motion(pos) + + _mouse(MOUSE_BUTTON_RIGHT, true, pos) + await process_frame + _check(camera.call("fly_active"), "RMB gedrückt → Flug aktiv (im Terrain-Werkzeug)") + _check(not bool(terrain_stroking()), "RMB startet KEINEN Terrain-Strich") + + # Im Flug bewegt W die Kamera (echter Bewegungsnachweis). + var pos0 := camera.position + Input.action_press("editor_fly_forward") + await create_timer(0.25).timeout + await process_frame + Input.action_release("editor_fly_forward") + _check(camera.position.distance_to(pos0) > 0.5, "Im Flug bewegt die Kamera (W)") + + _mouse(MOUSE_BUTTON_RIGHT, false, pos) + await process_frame + _check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus") + + var after := _heights() + _check(before == after, "Höhenfeld byte-identisch (kein Terrain-Change)") + _check(not str(main.history.undo_desc()).contains("Boden geformt"), + "Keine History-Zeile 'Boden geformt' durch RMB") + main.call("_set_tool_mode", "objects") + + +func terrain_stroking() -> bool: + return bool(main.get("terrain_tool").get("_stroking")) + + +## Abnahme 2: Strg+LMB senkt (bei Pinsel-Modus Heben), LMB ohne Strg hebt. +func _test_terrain_ctrl_lmb() -> void: + print("Testgruppe: Terrain Strg+LMB senkt / LMB hebt") + main.call("_set_tool_mode", "terrain") + var tool: Node = main.get("terrain_tool") + tool.call("set_brush_mode", TerrainField.Mode.RAISE) + main.terrain_strength_slider.value = 20.0 + var pos := _vp_center() + _motion(pos) + _check(bool(tool.get("_cursor_valid")), "Pinsel-Cursor trifft das Gelände") + var cursor: Vector3 = tool.get("_cursor") + var heights0 := _heights() + var h0 := _height_at(heights0, cursor.x, cursor.z) + + # Strg+LMB-Strich → senken. + _mouse(MOUSE_BUTTON_LEFT, true, pos, true) + await process_frame + _check(bool(tool.get("_stroking")), "Strg+LMB startet einen Strich") + _check(int(tool.get("_stroke_mode")) == TerrainField.Mode.LOWER, + "Strg+LMB bei Modus Heben → Strich-Modus SENKEN") + await create_timer(0.3).timeout + _mouse(MOUSE_BUTTON_LEFT, false, pos, true) + await process_frame + var heights1 := _heights() + var h1 := _height_at(heights1, cursor.x, cursor.z) + _check(h1 < h0 - 0.5, "Strg+LMB hat DEN BODEN GESSENKT (%.2f → %.2f m)" % [h0, h1]) + _check(str(main.history.undo_desc()).contains("Senken"), + "History: Strg+LMB-Strich läuft als 'Senken'") + + # LMB ohne Strg → heben (unverändert). Nach dem Senk-Strich trifft der + # Cursorstrahl eine NEUE Geländestelle (Grube) — Cursor frisch holen und + # dort vor/nach dem Strich messen. + _motion(pos) + var tool_fresh: Node = main.get("terrain_tool") + var cursor2: Vector3 = tool_fresh.get("_cursor") + var h1b := _height_at(_heights(), cursor2.x, cursor2.z) + _mouse(MOUSE_BUTTON_LEFT, true, pos) + await process_frame + await create_timer(0.2).timeout + _mouse(MOUSE_BUTTON_LEFT, false, pos) + await process_frame + var h2 := _height_at(_heights(), cursor2.x, cursor2.z) + _check(h2 > h1b + 0.3, "LMB ohne Strg HEBT weiter (%.2f → %.2f m)" % [h1b, h2]) + _check(str(main.history.undo_desc()).contains("Heben"), + "History: LMB-Strich läuft als 'Heben'") + main.call("_set_tool_mode", "objects") + + +## Abnahme 3: Objekt in der Hand + RMB → Flug, Objekt bleibt in der Hand, +## Esc wirft ab. +func _test_placement_rmb_keeps_hand() -> void: + print("Testgruppe: Platzieren + RMB = Flug (Objekt bleibt in der Hand)") + main.call("_set_tool_mode", "objects") + var oid := "" + for key in (main.manifest.get("objects", {}) as Dictionary): + oid = str(key) + break + _check(oid != "", "Manifest hat ein Objekt zum Platzieren") + if oid == "": + return + var interaction: Node = main.get("interaction") + interaction.call("start_placement", oid) + _check(bool(interaction.get("placing")), "Platzier-Modus an (Objekt in der Hand)") + + var pos := _vp_center() + _motion(pos) + _mouse(MOUSE_BUTTON_RIGHT, true, pos) + await process_frame + _check(camera.call("fly_active"), "RMB → Flug aktiv, WIEDERHOLTES Abwurf-RMB ist weg") + _check(bool(interaction.get("placing")), "Objekt ist WÄHREND des Flugs noch in der Hand") + + _mouse(MOUSE_BUTTON_RIGHT, false, pos) + await process_frame + _check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus") + _check(bool(interaction.get("placing")), "Objekt bleibt NACH dem Flug in der Hand (kein Abwurf)") + + # Esc wirft ab (zwei Frames, sonst reagiert der Viewport-Frame nicht). + main._unhandled_key_input(_press_key(KEY_ESCAPE)) + await process_frame + _check(not bool(interaction.get("placing")), "Esc wirft das Objekt ab (Platzieren aus)") + + +## Abnahme 4: Bauen-Werkzeug — RMB fliegt, wechselt NICHT das Werkzeug und +## behält die Bauteil-Auswahl. +func _test_build_rmb_flies() -> void: + print("Testgruppe: Bauen-Werkzeug + RMB = Flug (Werkzeug/Auswahl bleiben)") + main.call("_set_tool_mode", "bauen") + _check(str(main.get("tool_mode")) == "bauen", "Bauen-Werkzeug aktiv") + var build: Node = main.get("build_tool") + var pos := _vp_center() + _motion(pos) + + # Ein Bauteil setzen, dann per Umschalt+Klick auswählen — Klickpunkt ist + # die UNPROJIZIERTE Bauteil-Mitte (der gerastete Fußpunkt liegt sonst exakt + # auf der Kante des Center-Strahls). + _motion(pos) + _mouse(MOUSE_BUTTON_LEFT, true, pos) + _mouse(MOUSE_BUTTON_LEFT, false, pos) + await process_frame + var doc: Dictionary = main.doc + var gebaeude: Array = doc.get("gebaeude", []) + _check(gebaeude.size() > 0, "Bauteil gesetzt") + if gebaeude.is_empty(): + _finish() + return + var ppos: Array = (gebaeude[0] as Dictionary).get("pos", [0.0, 0.0, 0.0]) + var piece_center := Vector3(float(ppos[0]), float(ppos[1]) + 0.3, float(ppos[2])) + # Bauen-Pick-Radius: BuildOps.ray_pick hat max_dist=500 m — beim Default- + # Framing (1024-m-Map, Kamera ~614 m weg) würde der Pick leer bleiben. + # Deshalb für den Selektions-Klick nah heranzoomen (vorheriges Verhalten, + # kein Teil dieser Karte —siehe Task-Kommentar/Follow-up). + camera.set("distance", 120.0) + camera.call("focus_on", piece_center) + var on_piece := camera.unproject_position(piece_center) + _motion(on_piece) + _mouse(MOUSE_BUTTON_LEFT, true, on_piece, false, true) + _mouse(MOUSE_BUTTON_LEFT, false, on_piece, false, true) + await process_frame + var sel := int(build.get("selected_index")) + _check(sel >= 0, "Bauteil per Umschalt+Klick ausgewählt") + + _mouse(MOUSE_BUTTON_RIGHT, true, on_piece) + await process_frame + _check(camera.call("fly_active"), "RMB → Flug aktiv (im Bauen-Werkzeug)") + _check(str(main.get("tool_mode")) == "bauen", "RMB wechselt NICHT mehr das Werkzeug") + _check(int(build.get("selected_index")) == sel, "RMB wirft die Bauteil-Auswahl NICHT weg") + _mouse(MOUSE_BUTTON_RIGHT, false, pos) + await process_frame + _check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus") + + main.call("_set_tool_mode", "objects") + + +func _build_piece_count() -> int: + var doc: Dictionary = main.doc + return int((doc.get("gebaeude", []) as Array).size()) diff --git a/tests/test_rmb_vorrang.gd.uid b/tests/test_rmb_vorrang.gd.uid new file mode 100644 index 0000000..c288251 --- /dev/null +++ b/tests/test_rmb_vorrang.gd.uid @@ -0,0 +1 @@ +uid://dp0xxhx7kf2l5 diff --git a/tools/run_tests.sh b/tools/run_tests.sh index 54ec886..273d6f4 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." GODOT=~/bin/godot4 rc=0 -for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_gui_smoke; do +for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_gui_smoke test_rmb_vorrang; do "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1 code=$? line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)