Map-Editor: RMB = Flug mit absolutem Vorrang (t_2470de89) — Terrain-Senken auf Strg+LMB, Hand-Objekt-Abwurf auf Esc.

- 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).
This commit is contained in:
Hermes (godot-vr) 2026-10-17 17:36:57 +00:00
parent cda6f48b01
commit 7521db7f8d
12 changed files with 376 additions and 73 deletions

View file

@ -58,7 +58,8 @@ Boden/Wetter-Stimmung — siehe Commit 84bc081 und `svmap_io.gd`.)*
- **Objekt-Platzierung mit Ghost-Preview (`src/editor/interaction.gd`):** - **Objekt-Platzierung mit Ghost-Preview (`src/editor/interaction.gd`):**
Bibliothek links (deutsche Namen + Poly-Zahl), Klick startet den Bibliothek links (deutsche Namen + Poly-Zahl), Klick startet den
Platzier-Modus — eine transparente Vorschau schwebt am Cursor auf dem Boden 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 Mausrad dreht die Vorschau VOR dem Setzen (15°-Raster). Modus bleibt an
für schnelles Mehrfachsetzen. Poly-Budget wird VOR dem Platzieren geprüft. für schnelles Mehrfachsetzen. Poly-Budget wird VOR dem Platzieren geprüft.
- **Auswahl-Anfassen:** Klick wählt ein Objekt (orange + Emission), Ziehen - **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 beim Flug-Exit werden Ziel/Winkel/Abstand aus Flugposition/Blickrichtung
zurückgerechnet (kein Kamera-Sprung, getestet). Mathe pur in zurückgerechnet (kein Kamera-Sprung, getestet). Mathe pur in
`src/editor/fly_math.gd`, Kamera dünn (`src/viewport/orbit_camera.gd`). `src/editor/fly_math.gd`, Kamera dünn (`src/viewport/orbit_camera.gd`).
Konflikt-Auflösung: RMT beim Platzieren gehört dem Ghost-Abbruch Konflikt-Auflösung: RMB hat ABSOLUTEN Vorrang für den Flug — in JEDEM
(`rmb_filter`), Mausrad im Flug gehört dem Tempo (interaction blockt). 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- - **F** = Auswahl ins Bild zentrieren — Orbit-Ziel wandert auf den Auswahl-
Mittelpunkt (Anker + halbe Mesh-Höhe, `SelectionMath.focus_center`); im Mittelpunkt (Anker + halbe Mesh-Höhe, `SelectionMath.focus_center`); im
Flug dreht sich nur die Blickrichtung. Flug dreht sich nur die Blickrichtung.

View file

@ -27,7 +27,7 @@ Konventionen (alle Karten):
| Mittlere Maustaste ziehen | Orbit: Um die Karte drehen | | Mittlere Maustaste ziehen | Orbit: Um die Karte drehen |
| Mittlere Maustaste + Umschalt ziehen | Pan: Karte verschieben | | Mittlere Maustaste + Umschalt ziehen | Pan: Karte verschieben |
| Mausrad | Zoom (aus, wenn Mausrad gerade ein Objekt dreht) | | 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) | | Umschalt (im Flug) | schneller fliegen (×3) |
| Alt (im Flug) | langsamer fliegen (×0,3) | | Alt (im Flug) | langsamer fliegen (×0,3) |
| Mausrad (im Flug) | Grundgeschwindigkeit dauerhaft ändern (2–300 m/s) | | Mausrad (im Flug) | Grundgeschwindigkeit dauerhaft ändern (2–300 m/s) |
@ -91,6 +91,9 @@ nichts geht versehentlich verloren.
| **2** | Pinselmodus Senken | | **2** | Pinselmodus Senken |
| **3** | Pinselmodus Glätten | | **3** | Pinselmodus Glätten |
| **4** | Pinselmodus Malen (mit der zuletzt gewählten Materialfarbe) | | **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) | | 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 Konfliktfreiheit: Die Ziffern 1–4 sind NUR im Terrain-Werkzeug belegt — im

View file

@ -11,7 +11,8 @@ extends Node3D
## Bild↑/Bild↓ → Auswahl anheben/absenken (0,2 m — Wände aufs ## Bild↑/Bild↓ → Auswahl anheben/absenken (0,2 m — Wände aufs
## Fundament setzen) ## Fundament setzen)
## Entf → Auswahl löschen ## 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"). ## UNDO: EIN Rückgängig-Schritt PRO Bauteil (ActionHistory, Block "gebaeude").
@ -143,14 +144,9 @@ func _unhandled_input(event: InputEvent) -> void:
else: else:
_place_piece() _place_piece()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) —
if selected_index >= 0: # sie gehört mit absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug
selected_index = -1 # beenden läuft über ESC (main.gd), die Auswahl bleibt im Flug erhalten.
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()
func _update_cursor(viewport_pos: Vector2) -> void: func _update_cursor(viewport_pos: Vector2) -> void:
@ -223,7 +219,7 @@ func _select_at(viewport_pos: Vector2) -> void:
selected_index = int(pick["piece"]) selected_index = int(pick["piece"])
view_builder.call("set_gebaeude_selected", selected_index) view_builder.call("set_gebaeude_selected", selected_index)
var label := _piece_label(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: func _piece_label(idx: int) -> String:

View file

@ -191,7 +191,7 @@ func start_placement(oid: String) -> void:
var label: String = main.object_label(oid) var label: String = main.object_label(oid)
var hinweis := "" if not GlbMeshStore.fehlt(oid) \ var hinweis := "" if not GlbMeshStore.fehlt(oid) \
else " (Platzhalter — echte GLB fehlt für %s)" % 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) main.call("_highlight_library", oid)
@ -314,7 +314,7 @@ func _place_object(viewport_pos: Vector2) -> void:
main.call("_after_structure_change") 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)) 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), # 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) main.call("_mark_tree_selection", new_index)
@ -877,11 +877,9 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed:
_on_left_release(mb) _on_left_release(mb)
elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) —
if placing: # sie gehört mit absolutem Vorrang dem Freelook-Flug (orbit_camera.gd).
cancel_placement() # Beim Platzieren bleibt das Objekt IN der Hand, Abwurf nur über Esc.
main.call("_set_status", "Platzieren abgebrochen.")
get_viewport().set_input_as_handled()
elif mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed: elif mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
if _camera_flying(): if _camera_flying():
return # Mausrad = Flug-Tempo (orbit_camera), kein Objekt-Drehen 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.visible = true
_ghost_active = true _ghost_active = true
main.call("_highlight_library", oid) 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 ## Demo-Hook (Gruppen-Smoke, Karte t_f6f29d86): die Auswahl EXAKT so ziehen

View file

@ -7,7 +7,8 @@ extends Node3D
## Linksklick auf Zone = Zone auswählen; ZIEHEN verschiebt sie (Terrain-Höhe) ## Linksklick auf Zone = Zone auswählen; ZIEHEN verschiebt sie (Terrain-Höhe)
## Regel rechts = Radius der neuen Zonen; mit Auswahl: Radius DER Zone ## Regel rechts = Radius der neuen Zonen; mit Auswahl: Radius DER Zone
## Entf = ausgewählte Zone löschen ## 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 ## Jede Operation ist EIN Undo-Eintrag über die gemeinsame ActionHistory
## (Blöcke spawn_zones + design_notes, SpawnOps.snapshot_blocks). ## (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() get_viewport().set_input_as_handled()
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed:
_on_left_release() _on_left_release()
elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) —
if selected_team >= 0: # sie gehört mit absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug
_clear_selection() # beenden läuft über ESC (main.gd), die Auswahl bleibt im Flug erhalten.
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()
func _update_cursor(viewport_pos: Vector2) -> void: func _update_cursor(viewport_pos: Vector2) -> void:

View file

@ -4,8 +4,11 @@ extends Node3D
## ##
## Bedienung: ## Bedienung:
## Linksklick-Ziehen = gewählter Pinsel-Modus (Heben / Senken / Glätten) ## Linksklick-Ziehen = gewählter Pinsel-Modus (Heben / Senken / Glätten)
## Rechtsklick-Ziehen = Gegenteil (bei Heben senkt er, bei Senken hebt er — ## Strg+Linksklick = das Gegenteil (bei Heben senkt er, bei Senken hebt
## kein Umschalten nötig; bei Glätten senkt er) ## 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 ## Der ganze Strich = EIN Undo-Eintrag (Strg+Z stellt die Form VOR dem
## Strich wieder her, nicht nur eine Zwischenstufe). ## Strich wieder her, nicht nur eine Zwischenstufe).
## ##
@ -151,26 +154,26 @@ func _unhandled_input(event: InputEvent) -> void:
var mb := event as InputEventMouseButton var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_LEFT: if mb.button_index == MOUSE_BUTTON_LEFT:
if mb.pressed: 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: else:
_end_stroke() _end_stroke()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed: # Rechte Maustaste: absichtlich NICHT behandelt — sie gehört mit
# Rechtsklick = das Gegenteil zum Modell: bei Heben senken, # absolutem Vorrang dem Freelook-Flug (orbit_camera.gd, Karte
# bei Senken heben, bei Glätten senken (kein Umschalten nötig). # t_2470de89), in jedem Werkzeugzustand.
# 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()
func _update_cursor(viewport_pos: Vector2) -> void: func _update_cursor(viewport_pos: Vector2) -> void:

View file

@ -14,7 +14,7 @@ static func text() -> String:
["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"], ["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"],
["Mittlere Maustaste + Umschalt ziehen", "Pan: Karte verschieben"], ["Mittlere Maustaste + Umschalt ziehen", "Pan: Karte verschieben"],
["Mausrad", "Zoom (aus, während ein Objekt gedreht wird)"], ["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)"], ["Umschalt (im Flug)", "schneller fliegen (×3)"],
["Alt (im Flug)", "langsamer fliegen (×0,3)"], ["Alt (im Flug)", "langsamer fliegen (×0,3)"],
["Mausrad (im Flug)", "Grundgeschwindigkeit dauerhaft ändern (2–300 m/s)"], ["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"], ["1 / 2 / 3 / 4", "Pinselmodus: Heben / Senken / Glätten / Malen"],
["[ / ]", "Pinselgröße: Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch"], ["[ / ]", "Pinselgröße: Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch"],
["Linksklick-Ziehen", "Boden formen bzw. malen"], ["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 += _header("Datei & Undo")
out += _table(_rows([ out += _table(_rows([

View file

@ -388,7 +388,7 @@ func _build_left_panel() -> PanelContainer:
panel.add_child(vbox) panel.add_child(vbox)
vbox.add_child(_header_label("Objekt-Bibliothek")) 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() var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
@ -503,9 +503,10 @@ func _build_center_viewport() -> SubViewportContainer:
world.add_child(interaction) world.add_child(interaction)
interaction.setup(self, camera, view_builder) interaction.setup(self, camera, view_builder)
camera.set("wheel_filter", Callable(interaction, "wheel_blocked")) camera.set("wheel_filter", Callable(interaction, "wheel_blocked"))
# Freelook-Flug-Verdrahtung (Karte t_c46d31c2): RMT gehört beim Platzieren # Freelook-Flug-Verdrahtung (Karte t_c46d31c2): Status-Meldungen der
# dem Ghost-Abbruch, Status-Meldungen der Kamera laufen ins Statusfeld. # Kamera laufen ins Statusfeld. Karte t_2470de89: die rechte Maustaste
camera.set("rmb_filter", Callable(self, "_fly_rmb_blocked")) # gehört IMMER dem Flug — kein Filter mehr, die Werkzeuge fassen RMB
# nicht mehr an.
camera.set("status_cb", Callable(self, "_set_status")) camera.set("status_cb", Callable(self, "_set_status"))
# Terrain-Pinsel (Karte t_9aba04f1) — eigener Eingabe-Knoten im Weltraum. # 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(terrain_strength_slider)
terrain_panel.add_child(_muted_label( 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. " "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). ESC beendet den Terrain-Modus.")) + "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 --- # --- Spawn-Zonen (Karte t_c4162c5d): Live-Status IMMER sichtbar ---
vbox.add_child(_separator()) 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()) _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 ## Karte t_2470de89: Der alte Rechtsklick-Filter (_fly_rmb_blocked) ist weg —
## Maustaste dem Ghost-Abbruch (interaction) — der Flug startet dann nicht. ## RMB startet den Freelook-Flug in JEDEM Werkzeugzustand (absoluter Vorrang).
func _fly_rmb_blocked() -> bool:
return interaction.placing
func _refresh_history_buttons() -> void: func _refresh_history_buttons() -> void:
@ -1523,7 +1522,7 @@ func _enter_terrain_mode() -> void:
return return
tool_mode = "terrain" tool_mode = "terrain"
terrain_panel.visible = true 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_edit_btn.button_pressed = true
terrain_tool.call("set_brush_mode", _selected_terrain_mode()) 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.") _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_stroke", 90.0, 50.0, TerrainField.Mode.LOWER, 1.2)
terrain_tool.call("demo_park_brush", 90.0, 50.0) 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) _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") await _demo_shot(out_dir + "/terrain_lower.png")
# 3) Hügelkuppe glätten (blauer Ring). # 3) Hügelkuppe glätten (blauer Ring).

View file

@ -30,9 +30,6 @@ var distance := 300.0
## Von main.gd gesetzt: Callable → true, wenn das Mausrad gerade ein Objekt ## Von main.gd gesetzt: Callable → true, wenn das Mausrad gerade ein Objekt
## drehen soll (Platzier-Modus oder Auswahl) statt zu zoomen. ## drehen soll (Platzier-Modus oder Auswahl) statt zu zoomen.
var wheel_filter := Callable() 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. ## Von main.gd gesetzt: Callable für Klartext-Status-Meldungen.
var status_cb := Callable() var status_cb := Callable()
@ -73,9 +70,10 @@ func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton: if event is InputEventMouseButton:
var mb := event as InputEventMouseButton var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_RIGHT: 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 mb.pressed:
if _rmb_blocked():
return
_enter_fly() _enter_fly()
_fly_held = true _fly_held = true
elif _fly_held: elif _fly_held:
@ -134,10 +132,6 @@ func _wheel_blocked() -> bool:
return wheel_filter.is_valid() and bool(wheel_filter.call()) 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: func distance_max() -> float:
return 6000.0 return 6000.0

309
tests/test_rmb_vorrang.gd Normal file
View file

@ -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())

View file

@ -0,0 +1 @@
uid://dp0xxhx7kf2l5

View file

@ -3,7 +3,7 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
GODOT=~/bin/godot4 GODOT=~/bin/godot4
rc=0 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 "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1
code=$? code=$?
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1) line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)