Shortcuts 2/3 (t_55257792): R-Drehen + Pfeil-Raster + PgUp/Down + Strg+Bild↑ + Greifen + Terrain-Pinsel-Tasten.

- R = Auswahl 90° drehen (Szene um Auswahl-Mitte, Bauen-Bauteil um Fußpunkt, Gruppe komplett; Umschalt+R = -90°), Mausrad-Feindrehen 15° bleibt
- Pfeiltasten = 2-m-Raster-Verschiebung, Richtung = Kamera-Blickrichtung auf nächste Weltachse gerastet (MoveMath.nudge_delta, pure)
- Bild↑/↓ = ±0,25 m auf der GANZEN Auswahl (EIN Undo), Strg+Bild↑ = Fußpunkt aufs Terrain/Boden absetzen
- G = Greifen (Blender-Semantik): Auswahl hängt an der Maus, LMB setzt mit Raster-Snap, Esc rollt byte-genau zurück (ShortcutPolicy cancel_grab)
- Terrain-Pinsel: [ / ] = Preset-Zyklus Klein/Mittel/Groß (Slider/Label/Preset-Buttons wandern mit), 1-4 = Heben/Senken/Glätten/Malen (PAINT-Clamp-Fix, Palette mit Malen sichtbar); Ziffern NUR im Terrain-Werkzeug
- Strg halten = temporärer Snap-Zwang beim Bewegen/Greifen/Platzieren
- Tests: test_shortcuts.gd 157 Checks (R byte-genau, Pfeil-Raster, Lift-Stufen, Boden-Snap, Greifen+Esc, Pinsel-Zyklus, History-Eintrag pro Operation), test_gui_smoke.gd erweitert (38 Checks) und in run_tests.sh aufgenommen; F-Regression seit Karte 1 damit abgedeckt
- Suite 9/9 grün (696 Checks), Builder-Export 21/0, Parse-Check 11 Dateien grün
This commit is contained in:
Hermes (godot-vr) 2026-10-16 17:53:10 +00:00
parent 095af5ff3c
commit 08a75ac430
15 changed files with 1081 additions and 49 deletions

View file

@ -2,7 +2,8 @@
Deutsche Tastenbelegung des ScopedVR Map-Editors. **Diese Datei ist die Source Deutsche Tastenbelegung des ScopedVR Map-Editors. **Diese Datei ist die Source
of truth** für alle Shortcut-Karten (Karte 1/3: t_c46d31c2 — Navigation, of truth** für alle Shortcut-Karten (Karte 1/3: t_c46d31c2 — Navigation,
Auswahl, Löschen; Karten 2/3 ergänzen hier). Auswahl, Löschen; Karte 2/3: t_55257792 — Objektmanipulation + Terrain;
Karte 3/3 ergänzt hier).
Konventionen (alle Karten): Konventionen (alle Karten):
@ -13,6 +14,9 @@ Konventionen (alle Karten):
- Jede Objekt-Operation = **EIN** ActionHistory-Eintrag (Strg+Z nimmt die - Jede Objekt-Operation = **EIN** ActionHistory-Eintrag (Strg+Z nimmt die
ganze Operation zurück). Ausnahme: reine Kamera-Aktionen (Orbit/Flug/Zoom) ganze Operation zurück). Ausnahme: reine Kamera-Aktionen (Orbit/Flug/Zoom)
betreffen die Map nicht und sind nicht in der Historie. betreffen die Map nicht und sind nicht in der Historie.
- **Strg halten = temporärer Snap-Zwang** (Karte 2/3): egal was der
Raster-Snap-Schalter sagt — solange Strg unten ist, rastet Bewegen
(Ziehen/Greifen) und Platzieren auf ganze Meter.
## Navigation (Kamera) ## Navigation (Kamera)
@ -40,30 +44,50 @@ nicht — Ziel/Winkel/Abstand werden aus der Flugposition zurückgerechnet.
| Umschalt+Klick | wie Strg+Klick (Mehrfachauswahl umschalten) | | Umschalt+Klick | wie Strg+Klick (Mehrfachauswahl umschalten) |
| Ziehen über leeren Boden | Rahmen-Aufziehen: alle Objekte im Rahmen wählen | | Ziehen über leeren Boden | Rahmen-Aufziehen: alle Objekte im Rahmen wählen |
| Umschalt + Rahmen | Rahmen-Auswahl zur bestehenden Auswahl hinzufügen | | Umschalt + Rahmen | Rahmen-Auswahl zur bestehenden Auswahl hinzufügen |
| Esc | Auswahl aufheben / aktives Werkzeug abbrechen / Ghost-Preview abbrechen | | Esc | Werkzeug abbrechen → **Greifen zurückrollen** → Ghost-Preview abbrechen → Auswahl aufheben (Prioritätskette) |
Alle Bestandsaktionen wirken auf die GANZE Mehrfachauswahl: Entf löscht alle, Alle Bestandsaktionen wirken auf die GANZE Mehrfachauswahl: Entf löscht alle,
Strg+D dupliziert alle, Ziehen verschiebt gemeinsam, Strg+G gruppieren / Strg+D dupliziert alle, Ziehen verschiebt gemeinsam, Anheben/Absenken hebt
Strg+Umschalt+G lösen. alle, Strg+G gruppieren / Strg+Umschalt+G lösen.
## Objekt-Operationen ## Objekt-Manipulation (Karte 2/3)
| Eingabe | Wirkung | | Eingabe | Wirkung |
| --- | --- | | --- | --- |
| **R** | Auswahl 90° drehen (Vierteldrehung; Umschalt+R = −90°). Szene-Objekte drehen um den Auswahl-Mittelpunkt, Bauen-Bauteile um ihren Fußpunkt. Ein Objekt aus einer Gruppe dreht die GANZE Gruppe. Dauerhaft (kein Vorschau-Zustand) |
| Mausrad (mit Auswahl) | Feindrehen im 15°-Raster (bleibt wie bisher) |
| **Pfeiltasten** | Auswahl im **2-m-Raster** verschieben — EINE Zelle pro Tastendruck, Richtung = Kamera-Ausrichtung nächste Weltachse (Godot-Editor-Konvention: ↑ = von der Kamera weg). Fußpunkt bleibt am Raster, Höhe wandert mit |
| **Bild↑ / Bild↓** | Auswahl +0,25 m / −0,25 m anheben/absenken (Mehrfachdruck = EIN Undo-Schritt) |
| **Strg+Bild↑** | Auswahl direkt auf das Terrain/Boden absetzen (Godot „Snap Object to Floor“-Äquivalent fürs Fußpunkt-Modell) |
| **G** | Auswahl **greifen** (Blender-Semantik): sie hängt an der Maus, Linksklick setzt die neue Position (mit Raster-Snap bzw. Strg-Zwang), **Esc** bricht ab und setzt die exakten alten Positionen zurück. Greifen der ganzen Gruppe/mehreren Objekte wandert gemeinsam |
| **Entf (DELETE)** | Auswahl SOFORT löschen, ohne Rückfrage — Strg+Z holt alles zurück. Wirkt auf Szene-Auswahl UND Objektbaum-Markierung | | **Entf (DELETE)** | Auswahl SOFORT löschen, ohne Rückfrage — Strg+Z holt alles zurück. Wirkt auf Szene-Auswahl UND Objektbaum-Markierung |
| **Strg+D** | Auswahl duplizieren: Kopie +2 m in X versetzt (Rasterversatz), Füßpunkt unverändert; bei Mehrfachauswahl ALLE Objekte mit EINEM Undo-Schritt | | **Strg+D** | Auswahl duplizieren: Kopie +2 m in X versetzt (Rasterversatz), Füßpunkt unverändert; bei Mehrfachauswahl ALLE Objekte mit EINEM Undo-Schritt |
| R / Mausrad (mit Auswahl) | Drehen im 15°-Raster |
| Bild↑ / Bild↓ | Objekt +0,25 m / −0,25 m anheben/absenken (Mehrfachdruck = EIN Undo-Schritt) |
| Strg+Z / Strg+Y | Rückgängig / Wiederherstellen | | Strg+Z / Strg+Y | Rückgängig / Wiederherstellen |
| Strg+S | Karte speichern | | Strg+S | Karte speichern |
| Strg+G / Strg+Umschalt+G | Auswahl gruppieren / Gruppe lösen | | Strg+G / Strg+Umschalt+G | Auswahl gruppieren / Gruppe lösen |
## Terrain-Werkzeug (Karte 2/3) — nur aktiv, wenn das Terrain-Werkzeug an ist
| Eingabe | Wirkung |
| --- | --- |
| **[** | Pinselgröße verkleinern — durch die 3 Presets Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch; Preset-Buttons, Slider und Label wandern mit |
| **]** | Pinselgröße vergrößern (gleicher Zyklus, andere Richtung) |
| **1** | Pinselmodus Heben |
| **2** | Pinselmodus Senken |
| **3** | Pinselmodus Glätten |
| **4** | Pinselmodus Malen (mit der zuletzt gewählten Materialfarbe) |
| 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
Objekt-/Bauen-Modus sind sie unbenutzt, im Flugmodus (rechte Maustaste
gehalten) bleiben E/Q die Kamera.
## Werkzeuge ## Werkzeuge
| Eingabe | Wirkung | | Eingabe | Wirkung |
| --- | --- | | --- | --- |
| Esc (im Terrain-/Spawn-/Bauen-Werkzeug) | Werkzeug beenden — zurück zum Objekt-Werkzeug | | Esc (im Terrain-/Spawn-/Bauen-Werkzeug) | Werkzeug beenden — zurück zum Objekt-Werkzeug |
| Esc (beim Greifen) | Greifen abbrechen, alte Positionen zurück |
| Esc (beim Platzieren) | Ghost-Preview abbrechen | | Esc (beim Platzieren) | Ghost-Preview abbrechen |
*(Karten 2/3 ergänzen hier: R-Drehen 90°, Pfeiltasten-Raster, Bild↑/↓-Feinheiten, *(Karte 3/3 ergänzt hier ihre Belegungen.)*
Greifen mit G, Terrain-Pinsel `[` `]` und Ziffern-Modi.)*

View file

@ -266,6 +266,57 @@ func lift_selected(delta_m: float) -> void:
view_builder.call("set_gebaeude_selected", selected_index) view_builder.call("set_gebaeude_selected", selected_index)
## Pfeiltasten (Karte t_55257792): Bauteil im 2-m-Modulraster verschieben —
## EINE Zelle pro Tastendruck. Das exakte ±2-m-Delta erhält die Raster-
## Ausrichtung ("ecke" = gerade Meter, "mitte" = ungerade Meter bleibt), Y
## bleibt (Höhe ändert die Pfeiltaste nicht). EIN Undo-Schritt pro Tastendruck.
func nudge_selected(delta: Vector3) -> void:
if selected_index < 0:
main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus, dann verschieben die Pfeiltasten es im 2-m-Raster.")
return
var list := BuildOps.pieces(main.doc)
if selected_index >= list.size():
selected_index = -1
return
var teil: Dictionary = list[selected_index]
var pos: Array = teil.get("pos", [0.0, 0.0, 0.0])
var before: Dictionary = main.history.snapshot(["gebaeude"])
var nx := float(pos[0]) + delta.x
var nz := float(pos[2]) + delta.z
teil["pos"] = [nx, float(pos[1]), nz]
main.history.commit("%s verschoben (Pfeiltaste)" % _piece_label(selected_index),
before, main.history.snapshot(["gebaeude"]))
_after_build_change()
view_builder.call("set_gebaeude_selected", selected_index)
main.call("_set_status", "%s — 2 m pro Tastendruck, Raster bleibt eingehalten." % _piece_label(selected_index))
## Strg+Bild↑ (Karte t_55257792): Bauteil direkt auf das Terrain absetzen
## (Fußpunkt = Terrain-Höhe, auf 5 cm gerastet). EIN Undo-Schritt.
func snap_selected_to_floor() -> void:
if selected_index < 0:
main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus, dann setzt Strg+Bild↑ es auf den Boden.", true)
return
var list := BuildOps.pieces(main.doc)
if selected_index >= list.size():
selected_index = -1
return
var teil: Dictionary = list[selected_index]
var pos: Array = teil.get("pos", [0.0, 0.0, 0.0])
var ground := SvmapDocument.terrain_height_at(main.doc, float(pos[0]), float(pos[2]))
var target := BuildOps.snap_y(ground)
if absf(float(pos[1]) - target) < 0.0001:
main.call("_set_status", "%s liegt schon auf dem Boden." % _piece_label(selected_index))
return
var before: Dictionary = main.history.snapshot(["gebaeude"])
teil["pos"] = [float(pos[0]), target, float(pos[2])]
main.history.commit("%s auf den Boden gesetzt" % _piece_label(selected_index),
before, main.history.snapshot(["gebaeude"]))
_after_build_change()
view_builder.call("set_gebaeude_selected", selected_index)
main.call("_set_status", "%s auf den Boden gesetzt — Strg+Z nimmt es zurück." % _piece_label(selected_index))
func delete_selected() -> void: func delete_selected() -> void:
if selected_index < 0: if selected_index < 0:
main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus (Entf löscht es dann).", true) main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus (Entf löscht es dann).", true)

View file

@ -69,6 +69,21 @@ var _rubber_active := false # Rahmen wird gerade aufgezogen
var _rubber_start := Vector2.ZERO var _rubber_start := Vector2.ZERO
var _rubber_shift := false # Umschalt: Rahmen zur Auswahl HINZUFÜGEN var _rubber_shift := false # Umschalt: Rahmen zur Auswahl HINZUFÜGEN
# Strg halten = temporärer Snap-Zwang (Karte t_55257792): egal was der
# Raster-Snap-Schalter sagt — solange Strg unten ist, rastet Bewegen/Platzieren.
# Aus JEDEM durchgelassenen Input-Event aktualisiert (Maus- UND Taste).
var _ctrl_held := false
# Greifen (Karte t_55257792, G — Blender-Semantik): die Auswahl hängt an der
# Maus, Linksklick setzt die neue Position (EIN Undo-Schritt), Esc bricht ab
# und stellt die EXAKTEN alten Positionen wieder her.
var _grabbing := false
var _grab_members: Array = [] # Indizes der Greif-Menge (inkl. Gruppe)
var _grab_primary := -1
var _grab_originals: Dictionary = {} # idx (int) → [x, y, z] exakt (Rückabwicklung)
var _grab_lifts: Dictionary = {} # idx (int) → Terrain-Abstand beim Anfassen
var _grab_before: Dictionary = {} # History-Snapshot VOR dem Greifen
func setup(p_main, p_camera: Camera3D, p_builder) -> void: func setup(p_main, p_camera: Camera3D, p_builder) -> void:
main = p_main main = p_main
@ -223,16 +238,22 @@ func _ensure_ghost_visible() -> void:
_ghost.visible = true _ghost.visible = true
## Raster-Snap (wenn in main angeschaltet): X/Z auf ganze Meter rasten — ## Raster-Snap (wenn in main angeschaltet ODER Strg gehalten wird — temporärer
## Objekte passen dann nebeneinander wie Bauklötze. Höhe bleibt Terrain. ## Snap-Zwang, Karte t_55257792): X/Z auf ganze Meter rasten — Objekte passen
## dann nebeneinander wie Bauklötze. Höhe bleibt Terrain.
func _apply_grid(pos: Vector3) -> Vector3: func _apply_grid(pos: Vector3) -> Vector3:
if not bool(main.get("grid_snap")): if not (bool(main.get("grid_snap")) or _ctrl_held):
return pos return pos
pos.x = ObjectOps.snap_coord(pos.x, true, GRID_M) pos.x = ObjectOps.snap_coord(pos.x, true, GRID_M)
pos.z = ObjectOps.snap_coord(pos.z, true, GRID_M) pos.z = ObjectOps.snap_coord(pos.z, true, GRID_M)
return pos return pos
## Raster-Snap-Erzwingung fürs Platzieren: Schalter an ODER Strg gehalten.
func _placement_grid_enabled() -> bool:
return bool(main.get("grid_snap")) or _ctrl_held
func _ghost_update(viewport_pos: Vector2) -> void: func _ghost_update(viewport_pos: Vector2) -> void:
var p: Variant = _ground_point(viewport_pos) var p: Variant = _ground_point(viewport_pos)
if p == null: if p == null:
@ -243,7 +264,7 @@ func _ghost_update(viewport_pos: Vector2) -> void:
# EIN Pfad für Ghost UND Platzierung (Karte t_4092e3a0): Fußpunkt = # EIN Pfad für Ghost UND Platzierung (Karte t_4092e3a0): Fußpunkt =
# Terrain-Höhe am gerasteten Punkt. Der sichtbare Lift hängt am Kind-Mesh. # Terrain-Höhe am gerasteten Punkt. Der sichtbare Lift hängt am Kind-Mesh.
_ghost.position = ObjectOps.placement_foot( _ghost.position = ObjectOps.placement_foot(
main.doc, p as Vector3, bool(main.get("grid_snap")), _terrain_half(), true) main.doc, p as Vector3, _placement_grid_enabled(), _terrain_half(), true)
_ghost.visible = true _ghost.visible = true
@ -254,7 +275,7 @@ func _place_object(viewport_pos: Vector2) -> void:
return return
var half := _terrain_half() var half := _terrain_half()
var foot: Vector3 = ObjectOps.placement_foot( var foot: Vector3 = ObjectOps.placement_foot(
main.doc, p as Vector3, bool(main.get("grid_snap")), half, false) main.doc, p as Vector3, _placement_grid_enabled(), half, false)
if absf(foot.x) > half.x or absf(foot.z) > half.y: if absf(foot.x) > half.x or absf(foot.z) > half.y:
main.call("_set_status", "Das wäre außerhalb der Map gelandet — klicke innerhalb der grünen Fläche.", true) main.call("_set_status", "Das wäre außerhalb der Map gelandet — klicke innerhalb der grünen Fläche.", true)
return return
@ -521,7 +542,8 @@ func duplicate_selected() -> void:
main.call("_set_status", "%s: die Kopie(n) liegen +2 m versetzt und sind direkt angewählt — Ziehen verschiebt, Entf löscht, Strg+Z nimmt ALLE Kopien auf einmal zurück." % label) main.call("_set_status", "%s: die Kopie(n) liegen +2 m versetzt und sind direkt angewählt — Ziehen verschiebt, Entf löscht, Strg+Z nimmt ALLE Kopien auf einmal zurück." % label)
## ▲/▼ / Strg+Bild↑/↓: Objekt in 0,25-m-Schritten anheben/absenken. ## ▲/▼ / Strg+Bild↑/↓: AUSWAHL in 0,25-m-Schritten anheben/absenken
## (Karte t_55257792: die GANZE Auswahl, wie Entf/Strg+D auch).
## Mehrere Klicks in Serie = EIN Undo-Schritt (Merge-Tag, wie beim Drehen). ## Mehrere Klicks in Serie = EIN Undo-Schritt (Merge-Tag, wie beim Drehen).
## Absenken stoppt am Terrain (nichts wird begraben), Anheben an der ## Absenken stoppt am Terrain (nichts wird begraben), Anheben an der
## Match-Grenze (bounds.max) — Regeln in ObjectOps.lift_object. ## Match-Grenze (bounds.max) — Regeln in ObjectOps.lift_object.
@ -535,9 +557,12 @@ func lift_selected(delta_m: float) -> void:
if selected_index >= objects.size(): if selected_index >= objects.size():
clear_selection(true) clear_selection(true)
return return
var members := _lift_members()
var before: Dictionary = main.history.snapshot(["objects"]) var before: Dictionary = main.history.snapshot(["objects"])
var applied := ObjectOps.lift_object(main.doc, selected_index, delta_m) var applied_sum := 0.0
if absf(applied) < 0.0001: for i in members:
applied_sum += ObjectOps.lift_object(main.doc, i, delta_m)
if absf(applied_sum) < 0.0001:
# Nichts passiert (Snapshot verwerfen): hilfsbereit erklären, WO die Grenze ist. # Nichts passiert (Snapshot verwerfen): hilfsbereit erklären, WO die Grenze ist.
var ground: String var ground: String
if delta_m > 0.0: if delta_m > 0.0:
@ -547,20 +572,211 @@ func lift_selected(delta_m: float) -> void:
main.call("_set_status", ground) main.call("_set_status", ground)
return return
var after: Dictionary = main.history.snapshot(["objects"]) var after: Dictionary = main.history.snapshot(["objects"])
main.history.commit("Objekt %s (%s)" % [ var tag := "lift:%d" % selected_index if members.size() == 1 \
"angehoben" if delta_m > 0.0 else "abgesenkt", else "liftmulti:%s" % str(members)
("%.2f m" % applied)], main.history.commit(_lift_label(members, delta_m, applied_sum),
before, after, "lift:%d" % selected_index) before, after, tag)
var marker: Node3D = view_builder.object_markers[selected_index] if selected_index < view_builder.object_markers.size() else null for i in members:
if marker != null and is_instance_valid(marker): if i >= view_builder.object_markers.size():
marker.position.y = float((objects[selected_index]["pos"] as Array)[1]) continue
var marker: Node3D = view_builder.object_markers[i]
if marker != null and is_instance_valid(marker):
marker.position.y = float((objects[i]["pos"] as Array)[1])
main.dirty = true main.dirty = true
main.call("_refresh_file_label") main.call("_refresh_file_label")
main.call("_refresh_stats") main.call("_refresh_stats")
main.call("_refresh_history_buttons") main.call("_refresh_history_buttons")
main.call("_sync_selection_panel") main.call("_sync_selection_panel")
main.call("_set_status", "Objekt %s auf %.2f m — mehrmals drücken stapelt weiter; Strg+Z nimmt alle Stufen auf einmal zurück." % [ main.call("_set_status", "%s auf %.2f m — mehrmals drücken stapelt weiter; Strg+Z nimmt alle Stufen auf einmal zurück." % [
"angehoben" if delta_m > 0.0 else "abgesenkt", float((objects[selected_index]["pos"] as Array)[1])]) _lift_label(members, delta_m, applied_sum), float((objects[selected_index]["pos"] as Array)[1])])
## Heben/Absenken wirkt auf die GANZE Auswahl (Karte t_55257792) — ein einzelnes
## Objekt, das in einer Gruppe steckt, zieht die ganze Gruppe mit (wie Drehen).
func _lift_members() -> Array:
var members := selected_indices.duplicate()
if members.size() == 1:
var objects: Array = main.doc.get("objects", [])
var gid := GroupOps.group_id(objects[members[0]])
if gid != GroupOps.UNGROUPED:
members = GroupOps.members_of(main.doc, gid)
select_many(members, true)
main.call("_sync_selection_panel")
return members
func _lift_label(members: Array, delta_m: float, applied_sum: float) -> String:
var dir := "angehoben" if delta_m > 0.0 else "abgesenkt"
if members.size() == 1:
return "Objekt %s (%s)" % [dir, "%.2f m" % applied_sum]
return "%d Objekte %s (%.2f m)" % [members.size(), dir, applied_sum]
## Strg+Bild↑ (Karte t_55257792): die AUSWAHL direkt auf das Terrain/Boden
## absetzen (Godot "Snap Object to Floor"-Äquivalent fürs Fußpunkt-Modell).
## EIN Undo-Schritt; steht alles schon am Boden → Meldung statt leerem Commit.
func snap_selection_to_floor() -> void:
if selected_indices.is_empty() or selected_index < 0:
main.call("_set_status", "Nichts ausgewählt — Strg+Bild↑ setzt die Auswahl auf den Boden. Klicke zuerst ein Objekt an.", true)
return
if placing:
return
var members := _lift_members()
var before: Dictionary = main.history.snapshot(["objects"])
var changed := ObjectOps.snap_selection_to_floor(main.doc, members)
if changed == 0:
main.call("_set_status", "Steht schon alles auf dem Boden — nichts zu setzen.")
return
main.history.commit("Auswahl auf Boden gesetzt (%d Objekte)" % changed,
before, main.history.snapshot(["objects"]))
for i in members:
if i >= view_builder.object_markers.size():
continue
var marker: Node3D = view_builder.object_markers[i]
if marker != null and is_instance_valid(marker):
var pos: Array = ((main.doc.get("objects", []) as Array)[i] as Dictionary).get("pos", [0.0, 0.0, 0.0])
marker.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
_mark_dirty()
main.call("_refresh_stats")
main.call("_refresh_history_buttons")
main.call("_sync_selection_panel")
main.call("_set_status", "%d Objekt(e) auf den Boden gesetzt — Strg+Z nimmt es zurück." % changed)
## Pfeiltasten (Karte t_55257792): Auswahl im 2-m-Raster verschieben — EINE
## Zelle pro Tastendruck, Richtung = Kamera-Ausrichtung nächste Weltachse
## (MoveMath). Fußpunkt bleibt am Raster, Höhe wandert mit (t_7567a06f).
## EIN Undo-Schritt pro Tastendruck.
func nudge_selection(delta: Vector3) -> void:
if selected_indices.is_empty() or selected_index < 0:
main.call("_set_status", "Nichts ausgewählt — Pfeiltasten verschieben die Auswahl im 2-m-Raster. Klicke zuerst ein Objekt an.")
return
if placing:
return
var members := selected_indices.duplicate()
var objects: Array = main.doc.get("objects", [])
if members.size() == 1:
var gid := GroupOps.group_id(objects[members[0]])
if gid != GroupOps.UNGROUPED:
members = GroupOps.members_of(main.doc, gid)
var before: Dictionary = main.history.snapshot(["objects"])
var changed := ObjectOps.nudge_objects(main.doc, members, delta)
if changed == 0:
return # am Map-Rand geklemmt — Snapshot verwerfen, kein Commit
_sync_members_to_markers(members)
var desc := "Objekt verschoben (Pfeiltaste)" if members.size() == 1 \
else "%d Objekte verschoben (Pfeiltasten)" % members.size()
main.history.commit(desc, before, main.history.snapshot(["objects"]))
_mark_dirty()
main.call("_refresh_history_buttons")
main.call("_refresh_stats")
main.call("_sync_selection_panel")
main.call("_set_status", "%s — 2 m pro Tastendruck. Greifen (G) setzt frei, Strg+Bild↑ setzt auf den Boden." % desc)
# ------------------------------------------------------------ Greifen (G) ----
## Blender-Semantik (Karte t_55257792): G hängt die Auswahl an die Maus,
## Linksklick setzt die neue Position (EIN Undo-Schritt, Raster-Snap wie
## eingestellt bzw. Strg-Zwang), Esc bricht ab und setzt die EXAKTEN alten
## Positionen zurück. Die Auswahl wandert GEMEINSAM (Gruppe/Mehrfachauswahl),
## relative Anordnung bleibt exakt erhalten, Terrain-Abstand je Objekt bleibt.
## Greifen starten. false = nichts ausgewählt (Meldung steht dann).
func grab_begin() -> bool:
if _grabbing:
return true
if placing:
return false
var objects: Array = main.doc.get("objects", [])
var members := selected_indices.duplicate()
if members.size() == 1:
var gid := GroupOps.group_id(objects[members[0]])
if gid != GroupOps.UNGROUPED:
members = GroupOps.members_of(main.doc, gid)
select_many(members, true)
main.call("_sync_selection_panel")
if members.is_empty() or selected_index < 0:
main.call("_set_status", "Nichts ausgewählt — G greift die Auswahl, damit du sie mit der Maus frei setzen kannst. Klicke zuerst ein Objekt an.")
return false
_grab_members = members
_grab_primary = selected_index
_grab_originals = {}
_grab_lifts = {}
for i in members:
var pos: Array = (objects[i] as Dictionary).get("pos", [0.0, 0.0, 0.0])
_grab_originals[i] = [float(pos[0]), float(pos[1]), float(pos[2])]
_grab_lifts[i] = float(pos[1]) - SvmapDocument.terrain_height_at(
main.doc, float(pos[0]), float(pos[2]))
_grabbing = true
_grab_before = main.history.snapshot(["objects"])
main.call("_set_status", "GREIFEN an: die Auswahl hängt an der Maus — Linksklick setzt sie neu (mit Raster-Snap), Esc bricht ab und setzt zurück.")
return true
## Auswahl live zur Cursor-Bodenposition ziehen (Maus-Bewegung ODER Demo-Hook
## mit Weltpunkt). Der Primär-Anker geht EXAKT zum Punkt (rastet, wenn Snap
## an bzw. Strg), die Mitglieder wandern mit demselben Delta mit.
func grab_move(hit: Vector3) -> void:
if not _grabbing:
return
var target := _apply_grid(hit)
var half := _terrain_half()
var primary_pos: Array = _grab_originals[_grab_primary]
var delta := Vector3(target.x - float(primary_pos[0]), 0.0,
target.z - float(primary_pos[2]))
var objects: Array = main.doc.get("objects", [])
for i in _grab_members:
var orig: Array = _grab_originals[i]
var nx := clampf(float(orig[0]) + delta.x, -half.x, half.x)
var nz := clampf(float(orig[2]) + delta.z, -half.y, half.y)
var ny := SvmapDocument.terrain_height_at(main.doc, nx, nz) + float(_grab_lifts[i])
(objects[i] as Dictionary)["pos"] = [nx, ny, nz]
if i < view_builder.object_markers.size():
var marker: Node3D = view_builder.object_markers[i]
if marker != null and is_instance_valid(marker):
marker.position = Vector3(nx, ny, nz)
## Linksklick im Greifen: Positionen festschreiben — EIN Undo-Schritt.
func grab_confirm() -> void:
if not _grabbing:
return
_grabbing = false
var desc := "Objekt verschoben (greifen)" if _grab_members.size() == 1 \
else "%d Objekte verschoben (greifen)" % _grab_members.size()
main.history.commit(desc, _grab_before, main.history.snapshot(["objects"]))
_grab_members = []
_grab_originals = {}
_grab_lifts = {}
_grab_before = {}
_mark_dirty()
main.call("_refresh_history_buttons")
main.call("_refresh_stats")
main.call("_sync_selection_panel")
main.call("_set_status", "%s — Strg+Z nimmt den ganzen Zug zurück. G greift erneut, Esc hebt immer ab." % desc)
## Esc im Greifen: EXAKTE Rückabwicklung auf die Positionen von grab_begin —
## byte-identisch (keine Float-Re-Rechnung), KEIN History-Eintrag.
func grab_cancel() -> void:
if not _grabbing:
return
_grabbing = false
var objects: Array = main.doc.get("objects", [])
for i in _grab_members:
var orig: Array = _grab_originals[i]
(objects[i] as Dictionary)["pos"] = [float(orig[0]), float(orig[1]), float(orig[2])]
if i < view_builder.object_markers.size():
var marker: Node3D = view_builder.object_markers[i]
if marker != null and is_instance_valid(marker):
marker.position = Vector3(float(orig[0]), float(orig[1]), float(orig[2]))
_grab_members = []
_grab_originals = {}
_grab_lifts = {}
_grab_before = {}
main.call("_set_status", "Greifen abgebrochen — alles ist wieder, wie es war.")
## ⇄ Spiegeln (X: links↔rechts, Z: vor↔zurück) an der Stelle des Objekts — ## ⇄ Spiegeln (X: links↔rechts, Z: vor↔zurück) an der Stelle des Objekts —
@ -594,6 +810,14 @@ func mirror_selected(axis: String) -> void:
# ------------------------------------------------------------ Eingaben ------ # ------------------------------------------------------------ Eingaben ------
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
# Strg-Zwang (Karte t_55257792) aus JEDEM durchgelassenen Event verfolgen —
# Maus-Events tragen den Modifier, Tasten-Events ebenso (nicht konsumieren!).
if event is InputEventMouseMotion:
_ctrl_held = (event as InputEventMouseMotion).ctrl_pressed
elif event is InputEventMouseButton:
_ctrl_held = (event as InputEventMouseButton).ctrl_pressed
elif event is InputEventKey:
_ctrl_held = (event as InputEventKey).ctrl_pressed
var mode: String = main.get("tool_mode") var mode: String = main.get("tool_mode")
if mode == "terrain" or mode == "spawns": if mode == "terrain" or mode == "spawns":
return # Terrain-Pinsel / Spawn-Zonen haben das Zepter (eigene Werkzeuge) return # Terrain-Pinsel / Spawn-Zonen haben das Zepter (eigene Werkzeuge)
@ -601,6 +825,11 @@ func _unhandled_input(event: InputEvent) -> void:
var mm := event as InputEventMouseMotion var mm := event as InputEventMouseMotion
if placing: if placing:
_ghost_update(mm.position) _ghost_update(mm.position)
if _grabbing:
var gh: Variant = _ground_point(mm.position)
if gh != null:
grab_move(gh as Vector3)
return
if _rubber_pending or _rubber_active: if _rubber_pending or _rubber_active:
var dist := Vector2(mm.position.x - _press_pos.x, mm.position.y - _press_pos.y).length() var dist := Vector2(mm.position.x - _press_pos.x, mm.position.y - _press_pos.y).length()
if dist > DRAG_THRESHOLD_PX: if dist > DRAG_THRESHOLD_PX:
@ -634,6 +863,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_LEFT and mb.pressed: if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
if _grabbing:
grab_confirm()
get_viewport().set_input_as_handled()
return
_on_left_press(mb) _on_left_press(mb)
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:
@ -650,7 +883,7 @@ func _unhandled_input(event: InputEvent) -> void:
place_rot_deg = fmod(place_rot_deg + ROT_STEP_DEG + 3600.0, 360.0) place_rot_deg = fmod(place_rot_deg + ROT_STEP_DEG + 3600.0, 360.0)
_ghost.rotation.y = deg_to_rad(place_rot_deg) _ghost.rotation.y = deg_to_rad(place_rot_deg)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif selected_index >= 0: elif selected_index >= 0 and not _grabbing:
rotate_selected(ROT_STEP_DEG) rotate_selected(ROT_STEP_DEG)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed: elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
@ -660,7 +893,7 @@ func _unhandled_input(event: InputEvent) -> void:
place_rot_deg = fmod(place_rot_deg - ROT_STEP_DEG + 3600.0, 360.0) place_rot_deg = fmod(place_rot_deg - ROT_STEP_DEG + 3600.0, 360.0)
_ghost.rotation.y = deg_to_rad(place_rot_deg) _ghost.rotation.y = deg_to_rad(place_rot_deg)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif selected_index >= 0: elif selected_index >= 0 and not _grabbing:
rotate_selected(-ROT_STEP_DEG) rotate_selected(-ROT_STEP_DEG)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()

61
src/editor/move_math.gd Normal file
View file

@ -0,0 +1,61 @@
extends RefCounted
class_name MoveMath
## Shortcuts-Mathe für Karte 2/3 (t_55257792) — bewusst pure Funktionen
## (kein Knoten, keine Kamera): Pfeiltasten-Raster-Schritt + Terrain-
## Pinselgrößen-Zyklus. tests/test_shortcuts.gd prüft headless; main.gd und
## interaction.gd sind nur die dünnen Anwender.
##
## Pfeiltasten-Konvention (Editor-Üblichkeit): „↑" bewegt die Auswahl VON der
## Kamera WEG (in Blickrichtung, horizontal), „↓" auf sie zu, links/rechts
## entsprechend quer. Die Blickrichtung wird auf die NÄCHSTE Weltachse
## gerastet (Godot-Editor-Konvention) — eine Pfeiltaste verschiebt also immer
## exakt entlang X oder Z, nie diagonal.
const NUDGE_STEP_M := 2.0 # eine Rasterzelle pro Tastendruck (Karte 2/3)
## Pfeil-Richtung → Welt-Delta (2 m). yaw_deg = Kamera-Gierwinkel (Orbit ODER
## Flug), fwd = +1 (↑) / −1 (↓), side = +1 (rechts) / −1 (links).
## Die horizontale Blickrichtung (FlyMath.forward_from_euler(yaw, 0)) wird auf
## die nächste Weltachse gerastet: |z| >= |x| → Z-Achse, sonst X-Achse
## (Gleichstand → Z, deterministisch). Rechts = Blickrichtung × oben gekreuzt.
static func nudge_delta(yaw_deg: float, fwd: int, side: int) -> Vector3:
var f := forward_from_euler(yaw_deg, 0.0)
var axis := Vector3.ZERO
if absf(f.z) >= absf(f.x):
axis = Vector3(0.0, 0.0, -1.0) if f.z <= 0.0 else Vector3(0.0, 0.0, 1.0)
else:
axis = Vector3(-1.0, 0.0, 0.0) if f.x <= 0.0 else Vector3(1.0, 0.0, 0.0)
var right := axis.cross(Vector3.UP)
return (axis * float(fwd) + right * float(side)) * NUDGE_STEP_M
## Horizontale Blickrichtung (nur Yaw — identische Formel wie FlyMath, hier
## kopiert statt Abhängigkeit, damit die Datei ohne FlyMath-Load testbar bleibt).
static func forward_from_euler(yaw_deg: float, _pitch_deg: float) -> Vector3:
var yaw := deg_to_rad(yaw_deg)
return Vector3(-sin(yaw), 0.0, -cos(yaw))
# ------------------------------------------------- Terrain-Pinsel-Presets ----
## Pinselgröße-Zyklus durch die 3 Presets (Klein 4 m / Mittel 12 m / Groß 32 m,
## wie die Buttons im Terrain-Panel). up = ] (vergrößern), down = [ (verkleinern).
## Bei einem FREIEN Slider-Wert (nutzergezogen) geht ] zum nächsten Preset
## ÜBER dem Wert, [ zum nächsten DARUNTER — am Rand wird zyklisch gewrappt.
## Rückgabe: der neue Radius in Metern.
static func brush_next_preset(current_m: float, up: bool) -> float:
var presets := [4.0, 12.0, 32.0]
if up:
for r in presets:
if r > current_m + 0.001:
return r
return presets[0] # zyklisch: über dem größten → wieder Klein
# Abwärts: das NÄCHSTE Preset UNTER dem Wert (größtes darunter) —
# rückwärts iterieren, sonst fände die Schleife immer Klein zuerst.
var i := presets.size() - 1
while i >= 0:
if presets[i] < current_m - 0.001:
return presets[i]
i -= 1
return presets[presets.size() - 1] # zyklisch: unter dem kleinsten → Groß

View file

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

View file

@ -11,6 +11,58 @@ const DUPLICATE_OFFSET_M := Vector2(2.0, 0.0) # Kopie +2 m in X (Rasterversatz,
# Karte t_c46d31c2) # Karte t_c46d31c2)
## Auswahl per Pfeiltaste im 2-m-Raster verschieben (Karte t_55257792) — pure.
## Genau das gegebene Delta (±2 m pro Tastendruck, Richtung von MoveMath),
## X/Z an der Map geklemmt. Die HÖHE bleibt relativ zum Terrain erhalten
## (t_7567a06f-Semantik wie beim Ziehen): ein angehobenes Objekt schwebt
## weiter, ein bodennahes bleibt bodennah — ein Objekt auf dem 2-m-Raster
## landet wieder exakt auf dem Raster. Rückgabe: Anzahl geänderter Objekte.
static func nudge_objects(doc: Dictionary, indices: Array, delta: Vector3) -> int:
var objects: Array = doc.get("objects", [])
var half := _terrain_half_size(doc)
var changed := 0
for i in indices:
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
continue
var obj: Dictionary = objects[i]
var pos: Array = obj.get("pos", [0.0, 0.0, 0.0])
if (pos as Array).size() != 3:
continue
var x := clampf(float(pos[0]) + delta.x, -half.x, half.x)
var z := clampf(float(pos[2]) + delta.z, -half.y, half.y)
var lift := float(pos[1]) - SvmapDocument.terrain_height_at(doc, float(pos[0]), float(pos[2]))
var y := SvmapDocument.terrain_height_at(doc, x, z) + lift
if absf(x - float(pos[0])) > 0.0001 or absf(z - float(pos[2])) > 0.0001:
changed += 1
obj["pos"] = [x, y, z]
return changed
## Die AUSWAHL auf den Boden absetzen (Strg+Bild↑, Karte t_55257792 — das
## Godot-"Snap Object to Floor"-Äquivalent fürs Fußpunkt-Modell): pos.y wird
## exakt die Terrain-Höhe an (x, z). X/Z bleiben unangetastet. Rückgabe:
## Anzahl geänderter Objekte (0 = alles stand schon auf dem Boden).
static func snap_selection_to_floor(doc: Dictionary, indices: Array) -> int:
var objects: Array = doc.get("objects", [])
var changed := 0
for i in indices:
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
continue
var pos: Array = (objects[i] as Dictionary).get("pos", [0.0, 0.0, 0.0])
if (pos as Array).size() != 3:
continue
var h := SvmapDocument.terrain_height_at(doc, float(pos[0]), float(pos[2]))
if absf(float(pos[1]) - h) > FLOAT_EPSILON:
(objects[i] as Dictionary)["pos"] = [float(pos[0]), h, float(pos[2])]
changed += 1
return changed
static func _terrain_half_size(doc: Dictionary) -> Vector2:
var terrain: Dictionary = doc.get("terrain", {})
return Vector2(float(terrain.get("size_x_m", 1024.0)), float(terrain.get("size_z_m", 1024.0))) * 0.5
## ALLE Objekte auf die Terrain-Höhe setzen (Snap to Ground). Objekte hängen ## ALLE Objekte auf die Terrain-Höhe setzen (Snap to Ground). Objekte hängen
## z. B. nach dem Terrain-Formen in der Luft — ein Klick holt sie runter. ## z. B. nach dem Terrain-Formen in der Luft — ein Klick holt sie runter.
## Rückgabe: Anzahl geänderter Objekte (0 = alles stand schon auf dem Boden). ## Rückgabe: Anzahl geänderter Objekte (0 = alles stand schon auf dem Boden).

View file

@ -35,6 +35,21 @@ const ACTIONS := {
# Strg+LMB = Mehrfachauswahl umschalten (Maus-Event, handler prüft den # Strg+LMB = Mehrfachauswahl umschalten (Maus-Event, handler prüft den
# Modifier direkt am InputEventMouseButton — Action dokumentiert die Idee). # Modifier direkt am InputEventMouseButton — Action dokumentiert die Idee).
"editor_multi_select": [{"key": KEY_NONE, "mouse_left_ctrl": true}], "editor_multi_select": [{"key": KEY_NONE, "mouse_left_ctrl": true}],
# --- Objekt-Manipulation (Karte t_55257792, Karte 2/3) -------------------
"editor_rotate_90": [KEY_R],
"editor_nudge_up": [KEY_UP],
"editor_nudge_down": [KEY_DOWN],
"editor_nudge_left": [KEY_LEFT],
"editor_nudge_right": [KEY_RIGHT],
"editor_lift_to_floor": [{"key": KEY_PAGEUP, "ctrl": true}],
"editor_grab": [KEY_G],
# --- Terrain-Pinsel (nur im Terrain-Werkzeug aktiv, main gated) ----------
"editor_brush_smaller": [KEY_BRACKETLEFT],
"editor_brush_larger": [KEY_BRACKETRIGHT],
"editor_brush_raise": [KEY_1],
"editor_brush_lower": [KEY_2],
"editor_brush_smooth": [KEY_3],
"editor_brush_paint": [KEY_4],
} }

View file

@ -9,15 +9,21 @@ class_name ShortcutPolicy
## 2. Ghost-Preview (Platzieren) abbrechen ## 2. Ghost-Preview (Platzieren) abbrechen
## 3. Auswahl aufheben ## 3. Auswahl aufheben
## Rückgabe-Werte: ## Rückgabe-Werte:
## "exit_terrain" | "exit_spawns" | "exit_bauen" | "cancel_placement" ## "exit_terrain" | "exit_spawns" | "exit_bauen" | "cancel_grab"
## "clear_selection" | "none" ## "cancel_placement" | "clear_selection" | "none"
static func esc_action(tool_mode: String, placing: bool, has_selection: bool) -> String: ## grabbing (Karte t_55257792): G-Greifen läuft — Esc rollt die Positionen
## zurück (Blender-Semantik). Vorrang NACH Werkzeug-Exit (im Terrain-/Bauen-
## Werkzeug gibt es kein Greifen), VOR Platzieren/Greifen-Auswahlheben.
static func esc_action(tool_mode: String, placing: bool, has_selection: bool,
grabbing := false) -> String:
if tool_mode == "terrain": if tool_mode == "terrain":
return "exit_terrain" return "exit_terrain"
if tool_mode == "spawns": if tool_mode == "spawns":
return "exit_spawns" return "exit_spawns"
if tool_mode == "bauen": if tool_mode == "bauen":
return "exit_bauen" return "exit_bauen"
if grabbing:
return "cancel_grab"
if placing: if placing:
return "cancel_placement" return "cancel_placement"
if has_selection: if has_selection:

View file

@ -119,7 +119,9 @@ func _refresh_from_doc() -> bool:
# ------------------------------------------------------------ Pinsel-Optionen # ------------------------------------------------------------ Pinsel-Optionen
func set_brush_mode(mode: int) -> void: func set_brush_mode(mode: int) -> void:
brush_mode = clampi(mode, 0, 2) # Clamp bis PAINT (3) — der alte Clamp 0..2 hätte den Malen-Modus still
# zu Glätten degradiert (Karte t_55257792, Ziffer 4 = Malen).
brush_mode = clampi(mode, 0, MODE_LABELS.size() - 1)
_refresh_ring_mesh() _refresh_ring_mesh()

View file

@ -19,6 +19,8 @@ const APP_TITLE := "ScopedVR Map-Editor"
# Shortcut-Infrastruktur (Karte t_c46d31c2): InputMap-Actions + Esc-Priorität. # Shortcut-Infrastruktur (Karte t_c46d31c2): InputMap-Actions + Esc-Priorität.
const ShortcutKeys := preload("res://src/editor/shortcut_keys.gd") const ShortcutKeys := preload("res://src/editor/shortcut_keys.gd")
const ShortcutPolicy := preload("res://src/editor/shortcut_policy.gd") const ShortcutPolicy := preload("res://src/editor/shortcut_policy.gd")
# Shortcuts-Mathe Karte 2/3 (t_55257792): Pfeil-Raster + Pinsel-Presets.
const MoveMath := preload("res://src/editor/move_math.gd")
var manifest: Dictionary = {} var manifest: Dictionary = {}
var doc: Dictionary = {} # das Map-Dictionary (1:1 svmap-Format) var doc: Dictionary = {} # das Map-Dictionary (1:1 svmap-Format)
@ -1098,17 +1100,26 @@ func _unhandled_key_input(event: InputEvent) -> void:
else: else:
interaction.group_selection() interaction.group_selection()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif tool_mode == "objects" and not interaction.placing:
# G = Greifen (Karte t_55257792, Blender-Semantik): Auswahl
# hängt an der Maus, Linksklick setzt neu, Esc bricht ab.
interaction.grab_begin()
get_viewport().set_input_as_handled()
KEY_R: KEY_R:
if not ctrl: if not ctrl:
# R = Vierteldrehung 90° (Karte t_55257792): Bauen-Modus dreht
# Vorschau/Auswahl (wie bisher), Objekt-Auswahl UND Ghost drehen
# jetzt auch in 90°-Schritten (Umschalt+R = zurück). Feindrehen
# 15° bleibt am Mausrad.
var r_delta := -90.0 if event.shift_pressed else 90.0
if tool_mode == "bauen": if tool_mode == "bauen":
build_tool.call("rotate_input") build_tool.call("rotate_input")
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
var delta := -15.0 if event.shift_pressed else 15.0
if interaction.placing: if interaction.placing:
interaction.place_rot_deg = wrapf(interaction.place_rot_deg + delta, 0.0, 360.0) interaction.place_rot_deg = wrapf(interaction.place_rot_deg + r_delta, 0.0, 360.0)
else: else:
interaction.rotate_selected(delta) interaction.rotate_selected(r_delta)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
KEY_F: KEY_F:
if not ctrl: if not ctrl:
@ -1122,10 +1133,12 @@ func _unhandled_key_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
KEY_ESCAPE: KEY_ESCAPE:
# Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar): # Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar):
# Werkzeug verlassen → Ghost-Preview abbrechen → Auswahl aufheben. # Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview
# abbrechen → Auswahl aufheben (Greifen: Karte t_55257792).
var esc := ShortcutPolicy.esc_action(tool_mode, var esc := ShortcutPolicy.esc_action(tool_mode,
interaction.placing, interaction.placing,
interaction.selected_index != -1 or not interaction.selected_indices.is_empty()) interaction.selected_index != -1 or not interaction.selected_indices.is_empty(),
bool(interaction.get("_grabbing")))
match esc: match esc:
"exit_terrain": "exit_terrain":
_set_tool_mode("objects") _set_tool_mode("objects")
@ -1136,6 +1149,8 @@ func _unhandled_key_input(event: InputEvent) -> void:
"exit_bauen": "exit_bauen":
_set_tool_mode("objects") _set_tool_mode("objects")
_set_status("Bauen-Modus beendet — zurück zum Objekt-Werkzeug.") _set_status("Bauen-Modus beendet — zurück zum Objekt-Werkzeug.")
"cancel_grab":
interaction.grab_cancel()
"cancel_placement": "cancel_placement":
interaction.cancel_placement() interaction.cancel_placement()
_set_status("Platzieren abgebrochen.") _set_status("Platzieren abgebrochen.")
@ -1160,7 +1175,17 @@ func _unhandled_key_input(event: InputEvent) -> void:
interaction.duplicate_selected() interaction.duplicate_selected()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
KEY_PAGEUP: KEY_PAGEUP:
if not ctrl and not interaction.placing: if interaction.placing:
return
if ctrl:
# Strg+Bild↑ (Karte t_55257792): Auswahl/Bauteil auf den Boden.
if tool_mode == "bauen":
build_tool.call("snap_selected_to_floor")
get_viewport().set_input_as_handled()
elif tool_mode == "objects" and interaction.selected_index >= 0:
interaction.snap_selection_to_floor()
get_viewport().set_input_as_handled()
elif not ctrl:
if tool_mode == "bauen": if tool_mode == "bauen":
build_tool.call("lift_selected", ObjectOps.LIFT_STEP_M) build_tool.call("lift_selected", ObjectOps.LIFT_STEP_M)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
@ -1175,6 +1200,44 @@ func _unhandled_key_input(event: InputEvent) -> void:
elif tool_mode == "objects" and interaction.selected_index >= 0: elif tool_mode == "objects" and interaction.selected_index >= 0:
interaction.lift_selected(-ObjectOps.LIFT_STEP_M) interaction.lift_selected(-ObjectOps.LIFT_STEP_M)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT:
# Pfeiltasten (Karte t_55257792): Auswahl im 2-m-Raster verschieben,
# Richtung = Kamera-Ausrichtung nächste Weltachse (MoveMath). Eine
# Zelle pro Tastendruck; beim Platzieren bleibt der Ghost herrschend.
if interaction.placing:
return
var fwd := 0
var side := 0
match key:
KEY_UP:
fwd = 1
KEY_DOWN:
fwd = -1
KEY_RIGHT:
side = 1
KEY_LEFT:
side = -1
var delta := MoveMath.nudge_delta(
float(camera.call("view_yaw_deg")), fwd, side)
if tool_mode == "bauen":
build_tool.call("nudge_selected", delta)
get_viewport().set_input_as_handled()
elif tool_mode == "objects" and interaction.selected_index >= 0:
interaction.nudge_selection(delta)
get_viewport().set_input_as_handled()
KEY_BRACKETLEFT, KEY_BRACKETRIGHT:
# Terrain-Pinsel [ / ] (Karte t_55257792): Größe durch die 3 Presets
# zyklisch — nur im Terrain-Werkzeug (Ziffern-Freiheit, Punkt 6).
if tool_mode == "terrain":
_terrain_brush_size_step(key == KEY_BRACKETRIGHT)
get_viewport().set_input_as_handled()
KEY_1, KEY_2, KEY_3, KEY_4:
# Terrain-Pinsel-Modi 1–4 (Karte t_55257792): Heben/Senken/Glätten/
# Malen — NUR im Terrain-Werkzeug (Konfliktfreiheit, Punkt 6; im
# Objekt-/Bauen-Modus bleiben die Ziffern unbenutzt).
if tool_mode == "terrain":
_terrain_brush_mode_key(int(key) - int(KEY_1))
get_viewport().set_input_as_handled()
# --------------------------------------------------------- Bibliothek ------ # --------------------------------------------------------- Bibliothek ------
@ -1292,7 +1355,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 ·\nPinselgröße & Material rechts · Mittlere Maustaste: Kamera · Strg+Z: letzter Strich weg" 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"
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.")
@ -1330,7 +1393,7 @@ func _leave_spawn_mode() -> void:
func _enter_objects_mode() -> void: func _enter_objects_mode() -> void:
tool_mode = "objects" tool_mode = "objects"
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück" help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
_base_tool_status() _base_tool_status()
@ -1359,7 +1422,7 @@ func _enter_build_mode() -> void:
interaction.call("clear_selection", true) interaction.call("clear_selection", true)
tool_mode = "bauen" tool_mode = "bauen"
build_panel.visible = true build_panel.visible = true
help_label.text = "Linksklick: Bauteil setzen (auch auf andere Teile = stapeln) · Umschalt+Klick: Bauteil auswählen ·\nR: drehen (90°) · Bild↑/↓: anheben/absenken · Entf: löschen · Mittlere Maustaste: Kamera · Strg+Z: zurück" help_label.text = "Linksklick: Bauteil setzen (auch auf andere Teile = stapeln) · Umschalt+Klick: Bauteil auswählen ·\nR: drehen (90°) · Pfeiltasten: im 2-m-Raster verschieben · Bild↑/↓: anheben/absenken · Strg+Bild↑: auf Boden · Entf: löschen · Mittlere Maustaste: Kamera · Strg+Z: zurück"
build_edit_btn.button_pressed = true build_edit_btn.button_pressed = true
build_tool.call("activate") build_tool.call("activate")
_set_status("Bauen-Modus an: Bauteil rechts anklicken, dann in die Karte klicken — alles rastet aufs 2-m-Raster.") _set_status("Bauen-Modus an: Bauteil rechts anklicken, dann in die Karte klicken — alles rastet aufs 2-m-Raster.")
@ -1425,6 +1488,55 @@ func _on_terrain_size_preset(pressed: bool, radius_m: float) -> void:
terrain_radius_slider.value = radius_m terrain_radius_slider.value = radius_m
## Terrain-Pinsel-Größe per [ / ] (Karte t_55257792): durch die 3 Presets
## Klein/Mittel/Groß zyklisch — der Preset-Button wird mit gesetzt (set_pressed
## feuert toggled → Slider + Label + Pinssel-Ring wandern mit).
func _terrain_brush_size_step(up: bool) -> void:
if terrain_radius_slider == null:
return
var next := MoveMath.brush_next_preset(float(terrain_radius_slider.value), up)
var btn: Button = null
match next:
4.0:
btn = terrain_size_btn_small
12.0:
btn = terrain_size_btn_medium
32.0:
btn = terrain_size_btn_large
if btn != null:
btn.set_pressed(true) # feuert _on_terrain_size_preset → Slider/Label
else:
terrain_radius_slider.value = next
terrain_tool.call("set_radius", next)
_set_status("Pinsel-Größe: %d m (mit [ / ] durch Klein/Mittel/Groß zyklisch)." % int(next))
## Terrain-Pinsel-Modus per Ziffer 1–4 (Karte t_55257792): Heben/Senken/
## Glätten/Malen — der Modus-Button wird mit gesetzt (toggled feuert die
## bestehende Sync-Kette inkl. Material-Palette im Malen-Modus). Malen startet
## mit der zuletzt gewählten Materialfarbe (paint_material bleibt stehen).
func _terrain_brush_mode_key(mode_idx: int) -> void:
var btn: Button = null
match mode_idx:
0:
btn = terrain_mode_raise
1:
btn = terrain_mode_lower
2:
btn = terrain_mode_smooth
3:
btn = terrain_mode_paint
if btn == null or terrain_tool == null:
return
btn.set_pressed(true)
var labels := ["Heben", "Senken", "Glätten", "Malen"]
var extra := ""
if mode_idx == 3:
var mats: Array = SvmapIO.MATERIAL_IDS
extra = " (Material: %s — mit der Palette rechts wechseln)" % str(mats[clampi(int(terrain_tool.get("paint_material")), 0, mats.size() - 1)])
_set_status("Pinsel-Modus: %s%s — Linksklick-Ziehen formt/malt jetzt so (Ziffern 1–4 wechseln)." % [str(labels[mode_idx]), extra])
func _on_terrain_radius_changed(value: float) -> void: func _on_terrain_radius_changed(value: float) -> void:
terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value) terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value)
if terrain_tool != null: if terrain_tool != null:
@ -1458,7 +1570,7 @@ func _recheck_terrain_mode() -> void:
tool_mode = "objects" tool_mode = "objects"
terrain_panel.visible = false terrain_panel.visible = false
_sync_terrain_button() _sync_terrain_button()
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück" help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
_set_status("Terrain-Modus aus: Diese Map passt nicht ins Editor-MVP (Terrain-Auflösung über %d) — die Objekte bleiben normal bearbeitbar." % TerrainField.EDITOR_MAX_RESOLUTION, true) _set_status("Terrain-Modus aus: Diese Map passt nicht ins Editor-MVP (Terrain-Auflösung über %d) — die Objekte bleiben normal bearbeitbar." % TerrainField.EDITOR_MAX_RESOLUTION, true)

View file

@ -160,6 +160,13 @@ func fly_active() -> bool:
return _flying return _flying
## Aktueller Gierwinkel der BLICKRICHTUNG (Karte t_55257792): im Flug der
## Flug-Yaw, im Orbit der Orbit-Yaw. Für die Pfeiltasten-Richtung (MoveMath
## rastet sie auf die nächste Weltachse).
func view_yaw_deg() -> float:
return _fly_yaw if _flying else yaw_deg
## Auswahl ins Bild zentrieren (F): Orbit → Ziel-Punkt auf den Auswahl-Mittelpunkt; ## Auswahl ins Bild zentrieren (F): Orbit → Ziel-Punkt auf den Auswahl-Mittelpunkt;
## Flug → Blickrichtung drehen, Position bleibt. ## Flug → Blickrichtung drehen, Position bleibt.
func focus_on(center: Vector3) -> void: func focus_on(center: Vector3) -> void:

View file

@ -40,6 +40,7 @@ func _run_all() -> void:
_test_ui_built() _test_ui_built()
_test_terrain_presets() _test_terrain_presets()
_test_shortcut_undo() _test_shortcut_undo()
_test_terrain_hotkeys()
_finish() _finish()
@ -123,12 +124,11 @@ func _test_shortcut_undo() -> void:
main.history.snapshot(["objects"])) main.history.snapshot(["objects"]))
_check((main.doc["objects"] as Array).size() == 1, "Test-Objekt platziert") _check((main.doc["objects"] as Array).size() == 1, "Test-Objekt platziert")
# Vorher-Zustand für die unbehandelte Taste merken. # Karte 1 (t_c46d31c2) hat F belegt: F zentriert die Auswahl — ohne
var status_vorher := str(main.status_label.text) # Auswahl gibt es den freundlichen Hinweis (kein Crash, kein Wirken).
main._unhandled_key_input(_press_key(KEY_F, false)) main._unhandled_key_input(_press_key(KEY_F, false))
_check(str(main.status_label.text) == status_vorher, _check(str(main.status_label.text).contains("Nichts ausgewählt"),
"unbehandelte Taste (F) ändert nichts, läuft aber sauber durch") "F ohne Auswahl: freundlicher Hinweis, läuft sauber durch")
main._unhandled_key_input(_press_key(KEY_Z, true)) 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((main.doc["objects"] as Array).is_empty(), "Strg+Z hat das Objekt entfernt (Undo wirkt)")
@ -145,3 +145,63 @@ func _test_shortcut_undo() -> void:
main._unhandled_key_input(_press_key(KEY_Z, true)) main._unhandled_key_input(_press_key(KEY_Z, true))
_check(str(main.status_label.text).contains("Nichts zum Rückgängigmachen"), _check(str(main.status_label.text).contains("Nichts zum Rückgängigmachen"),
"Strg+Z bei leerer History: Statusmeldung, kein Absturz") "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")

View file

@ -14,13 +14,22 @@ func _init() -> void:
_test_inputmap_actions() _test_inputmap_actions()
_test_fly_math() _test_fly_math()
_test_esc_policy() _test_esc_policy()
_test_esc_grab_policy()
_test_duplicate_offset() _test_duplicate_offset()
_test_move_math()
# Node-basierte Tests (global_position, look_at) brauchen einen fertigen # Node-basierte Tests (global_position, look_at) brauchen einen fertigen
# Baum — in _init ist root noch nicht "inside tree" (Gui-Smoke-Muster). # Baum — in _init ist root noch nicht "inside tree" (Gui-Smoke-Muster).
await process_frame await process_frame
_test_multi_delete() _test_multi_delete()
_test_focus_center() _test_focus_center()
_test_camera_fly_roundtrip() _test_camera_fly_roundtrip()
_test_rotate_90()
_test_nudge_grid()
_test_lift_multi()
_test_snap_to_floor()
_test_grab()
_test_build_nudge_floor()
_test_brush_paint_mode()
if failures == 0: if failures == 0:
print("ALL OK: %d checks" % checks) print("ALL OK: %d checks" % checks)
quit(0) quit(0)
@ -88,10 +97,14 @@ class ProbeMain:
class ProbeBuilder extends Node3D: class ProbeBuilder extends Node3D:
var object_markers: Array = [] var object_markers: Array = []
var last_selection: Array = [] var last_selection: Array = []
var last_gebaeude_selection := -99
func set_selected_multi(indices: Array) -> void: func set_selected_multi(indices: Array) -> void:
last_selection = indices.duplicate() last_selection = indices.duplicate()
func set_gebaeude_selected(idx: int) -> void:
last_gebaeude_selection = idx
# ------------------------------------------------------------ InputMap ------- # ------------------------------------------------------------ InputMap -------
@ -149,6 +162,36 @@ func _test_inputmap_actions() -> void:
if ev is InputEventMouseButton and (ev as InputEventMouseButton).button_index == MOUSE_BUTTON_RIGHT: if ev is InputEventMouseButton and (ev as InputEventMouseButton).button_index == MOUSE_BUTTON_RIGHT:
freelook_rmb = true freelook_rmb = true
_check(freelook_rmb, "editor_freelook → Rechte Maustaste") _check(freelook_rmb, "editor_freelook → Rechte Maustaste")
# Karte 2/3 (t_55257792): neue Actions mit physischen Tasten.
var expected_keys_2 := {
"editor_rotate_90": KEY_R,
"editor_nudge_up": KEY_UP,
"editor_nudge_down": KEY_DOWN,
"editor_nudge_left": KEY_LEFT,
"editor_nudge_right": KEY_RIGHT,
"editor_grab": KEY_G,
"editor_brush_smaller": KEY_BRACKETLEFT,
"editor_brush_larger": KEY_BRACKETRIGHT,
"editor_brush_raise": KEY_1,
"editor_brush_lower": KEY_2,
"editor_brush_smooth": KEY_3,
"editor_brush_paint": KEY_4,
}
for action in expected_keys_2:
if not InputMap.has_action(action):
_check(false, "Action existiert: %s" % action)
continue
var ok := false
for ev in InputMap.action_get_events(action):
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == int(expected_keys_2[action]):
ok = true
_check(ok, "%s → physische Taste %s" % [action, OS.get_keycode_string(int(expected_keys_2[action]))])
var lift_floor := false
for ev in InputMap.action_get_events("editor_lift_to_floor"):
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == KEY_PAGEUP \
and (ev as InputEventKey).ctrl_pressed:
lift_floor = true
_check(lift_floor, "editor_lift_to_floor → Strg+Bild↑")
# ------------------------------------------------------------ FlyMath -------- # ------------------------------------------------------------ FlyMath --------
@ -349,3 +392,348 @@ func _test_camera_fly_roundtrip() -> void:
_check(bool(cam.call("fly_active")), "Umschalt+F: Dauerflug an (ohne RMT)") _check(bool(cam.call("fly_active")), "Umschalt+F: Dauerflug an (ohne RMT)")
cam.call("toggle_fly_lock") cam.call("toggle_fly_lock")
_check(not bool(cam.call("fly_active")), "Umschalt+F nochmal: zurück zum Orbit") _check(not bool(cam.call("fly_active")), "Umschalt+F nochmal: zurück zum Orbit")
# Blick-Yaw-Accessor (Karte t_55257792): Orbit → yaw_deg, Flug → Flug-Yaw.
_check(absf(float(cam.call("view_yaw_deg")) - 30.0) < 0.0001,
"view_yaw_deg im Orbit = Orbit-Yaw (30°)")
cam.set("_fly_yaw", -120.0)
cam.set("_flying", true)
_check(absf(float(cam.call("view_yaw_deg")) + 120.0) < 0.0001,
"view_yaw_deg im Flug = Flug-Yaw")
# --------------------------------------------------------- Esc + Greifen -----
func _test_esc_grab_policy() -> void:
print("Testgruppe: Esc-Priorität mit Greifen (ShortcutPolicy)")
_check(ShortcutPolicy.esc_action("objects", false, false, true) == "cancel_grab",
"Esc beim Greifen → Greifen zurückrollen")
_check(ShortcutPolicy.esc_action("objects", false, true, true) == "cancel_grab",
"Greifen hat VORRANG vor Auswahl-Aufheben")
_check(ShortcutPolicy.esc_action("objects", true, true, true) == "cancel_grab",
"Greifen hat VORRANG vor Platzieren (kann nicht koexistieren)")
_check(ShortcutPolicy.esc_action("terrain", false, false, true) == "exit_terrain",
"Werkzeug-Exit bleibt VOR Greifen (im Terrain gibt es kein Greifen)")
_check(ShortcutPolicy.esc_action("objects", true, true, false) == "cancel_placement",
"alter 3-Argument-Aufruf bleibt unverändert (Rückwärtskompatibel)")
# ------------------------------------------------------ MoveMath (Karte 2) ----
func _test_move_math() -> void:
print("Testgruppe: Pfeil-Raster + Pinsel-Presets (MoveMath)")
# Blick -Z (yaw 0): ↑ = -Z, ↓ = +Z, rechts = +X, links = -X.
_check(MoveMath.nudge_delta(0.0, 1, 0) == Vector3(0, 0, -2), "yaw 0: ↑ = 2 m nach -Z")
_check(MoveMath.nudge_delta(0.0, -1, 0) == Vector3(0, 0, 2), "yaw 0: ↓ = 2 m nach +Z")
_check(MoveMath.nudge_delta(0.0, 0, 1) == Vector3(2, 0, 0), "yaw 0: → = 2 m nach +X")
_check(MoveMath.nudge_delta(0.0, 0, -1) == Vector3(-2, 0, 0), "yaw 0: ← = 2 m nach -X")
# Blick -X (yaw 90): ↑ = -X, rechts = -Z (rechts vom Blick).
_check(MoveMath.nudge_delta(90.0, 1, 0) == Vector3(-2, 0, 0), "yaw 90: ↑ = 2 m nach -X")
_check(MoveMath.nudge_delta(90.0, 0, 1) == Vector3(0, 0, -2), "yaw 90: → = 2 m nach -Z")
# Blick +Z (yaw 180): ↑ = +Z.
_check(MoveMath.nudge_delta(180.0, 1, 0) == Vector3(0, 0, 2), "yaw 180: ↑ = 2 m nach +Z")
# Blick +X (yaw -90): ↑ = +X, → = +Z.
_check(MoveMath.nudge_delta(-90.0, 1, 0) == Vector3(2, 0, 0), "yaw -90: ↑ = 2 m nach +X")
_check(MoveMath.nudge_delta(-90.0, 0, 1) == Vector3(0, 0, 2), "yaw -90: → = 2 m nach +Z")
# Schräger Blick rastet auf die NÄCHSTE Weltachse (Editor-Konvention):
# yaw 20 → Blick überwiegend -Z → ↑ = -Z; yaw 70 → überwiegend -X → ↑ = -X.
_check(MoveMath.nudge_delta(20.0, 1, 0) == Vector3(0, 0, -2), "yaw 20 (schräg): ↑ rastet auf -Z")
_check(MoveMath.nudge_delta(70.0, 1, 0) == Vector3(-2, 0, 0), "yaw 70 (schräg): ↑ rastet auf -X")
# Pinsel-Presets: Zyklus Klein(4) → Mittel(12) → Groß(32) → Klein ...
_check(MoveMath.brush_next_preset(4.0, true) == 12.0, "[ ]-Zyklus: Klein → Mittel")
_check(MoveMath.brush_next_preset(12.0, true) == 32.0, "[ ]-Zyklus: Mittel → Groß")
_check(MoveMath.brush_next_preset(32.0, true) == 4.0, "[ ]-Zyklus: Groß → Klein (wrap)")
_check(MoveMath.brush_next_preset(32.0, false) == 12.0, "[ ]-Zyklus zurück: Groß → Mittel")
_check(MoveMath.brush_next_preset(4.0, false) == 32.0, "[ ]-Zyklus zurück: Klein → Groß (wrap)")
# Freier Slider-Wert: nächstes Preset ÜBER bzw. UNTER dem Wert.
_check(MoveMath.brush_next_preset(20.0, true) == 32.0, "freier Wert 20: ] → nächstes Preset darüber (32)")
_check(MoveMath.brush_next_preset(20.0, false) == 12.0, "freier Wert 20: [ → nächstes Preset darunter (12)")
# --------------------------------------------------------- R = 90°-Drehen ----
func _make_interaction(doc: Dictionary, markers: int) -> Array:
var fake := ProbeMain.new()
fake.doc = doc
fake.history.begin(fake.doc)
var vp := SubViewport.new()
vp.size = Vector2i(1280, 720)
root.add_child(vp)
var cam := Camera3D.new()
vp.add_child(cam)
cam.current = true
var builder := ProbeBuilder.new()
vp.add_child(builder)
for i in markers:
var m := Node3D.new()
m.set_meta("half_height", 1.0)
builder.object_markers.append(m)
var interaction := Node3D.new()
interaction.set_script(load("res://src/editor/interaction.gd"))
vp.add_child(interaction)
interaction.setup(fake, cam, builder)
return [fake, interaction, builder]
func _test_rotate_90() -> void:
print("Testgruppe: R = Auswahl 90° drehen (byte-genau)")
var doc := {"objects": [
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 30.0},
{"id": "tree_pine", "pos": [20.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
{"id": "crate_wood", "pos": [10.0, 0.0, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
]}
var pair := _make_interaction(doc, 3)
var fake: ProbeMain = pair[0]
var interaction = pair[1]
# Einzelnes Objekt: R dreht rot_y_deg exakt +90, Position bleibt (Drehung
# um die eigene Mitte = Fußpunkt bleibt an Ort und Stelle).
interaction.select_object(0)
interaction.rotate_selected(90.0)
var objects: Array = doc["objects"]
_check(absf(float(objects[0]["rot_y_deg"]) - 120.0) < 0.0001, "R: 30° → 120° (exakt +90)")
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 10.0,
"R: Einzelobjekt bleibt auf seinem Fußpunkt stehen")
_check(float(objects[0]["pos"][1]) == 0.0, "R: Füßpunkt-Y byte-genau unverändert")
_check(fake.history.can_undo(), "R: EIN History-Eintrag pro Drehung")
# Mehrfachauswahl: beide drehen um die GEMEINSAME Mitte — beide rotieren
# +90, die Mitte bleibt (byte-genau: gleicher Code-Pfad wie Mausrad).
interaction.select_many([0, 2])
var c_before := GroupOps.centroid(doc, [0, 2])
interaction.rotate_selected(90.0)
var c_after := GroupOps.centroid(doc, [0, 2])
_check(absf(float(objects[0]["rot_y_deg"]) - 210.0) < 0.0001
and absf(float(objects[2]["rot_y_deg"]) - 90.0) < 0.0001,
"R bei Mehrfachauswahl: beide Objekte +90°")
_check((c_after - c_before).length() < 0.0001,
"R: Auswahl-Mittelpunkt bleibt exakt (Drehung um die Mitte)")
# EIN History-Eintrag pro Operation: das erste Undo nimmt nur die
# Mehrfach-Drehung zurück (210°→120°, Fußpunkte wieder am alten Ort),
# das zweite die Einzel-Drehung davor (120°→30°).
fake.history.undo()
_check(absf(float(objects[0]["rot_y_deg"]) - 120.0) < 0.0001
and absf(float(objects[2]["rot_y_deg"])) < 0.0001
and float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 10.0
and float(objects[2]["pos"][0]) == 10.0 and float(objects[2]["pos"][2]) == 20.0,
"Strg+Z: Mehrfach-Drehung komplett zurück (Rotation + Fußpunkte byte-genau)")
fake.history.undo()
_check(absf(float(objects[0]["rot_y_deg"]) - 30.0) < 0.0001,
"Strg+Z: zweite Ebene — Einzel-Drehung auch zurück (30°)")
# Bauen-Modus: Bauteil rotiert um seinen Fußpunkt (Position unverändert).
var build_doc := {"gebaeude": [
{"id": "wand", "pos": [4.0, 0.0, 6.0], "rot": 0.0, "punkte": [[0, 0, 0], [2, 0, 0]]},
]}
var bfake := ProbeMain.new()
bfake.doc = build_doc
bfake.history.begin(bfake.doc)
var bbuilder := ProbeBuilder.new()
root.add_child(bbuilder)
var btool := Node3D.new()
btool.set_script(load("res://src/editor/build_tool.gd"))
root.add_child(btool)
btool.setup(bfake, Camera3D.new(), bbuilder, interaction)
btool.select_index(0)
btool.rotate_input()
var list: Array = BuildOps.pieces(build_doc)
_check(absf(float(list[0]["rot"]) - 90.0) < 0.0001, "Bauen: R dreht Bauteil 90°")
_check(float(list[0]["pos"][0]) == 4.0 and float(list[0]["pos"][2]) == 6.0,
"Bauen: Fußpunkt bleibt (Drehung um den Fußpunkt)")
# ---------------------------------------------------- Pfeiltasten 2-m-Raster --
func _test_nudge_grid() -> void:
print("Testgruppe: Pfeiltasten — 2-m-Raster, Höhe wandert mit, EIN Undo pro Druck")
var doc := {"objects": [
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
{"id": "tree_pine", "pos": [10.0, 3.0, 12.0], "scale": 1.0, "rot_y_deg": 0.0},
]}
var pair := _make_interaction(doc, 2)
var fake: ProbeMain = pair[0]
var interaction = pair[1]
var objects: Array = doc["objects"]
interaction.select_object(0)
interaction.nudge_selection(Vector3(0, 0, -2))
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 8.0,
"↑: exakt 2 m in -Z, Fußpunkt bleibt im Raster")
_check(fake.history.can_undo(), "Pfeiltaste: EIN History-Eintrag")
fake.history.undo()
_check(float(objects[0]["pos"][2]) == 10.0, "Strg+Z: eine Zelle zurück")
# Höhe wandert mit (t_7567a06f-Semantik): angehobenes Objekt (y=3 über
# flachem Terrain) bleibt exakt auf 3 m.
interaction.select_object(1)
interaction.nudge_selection(Vector3(2, 0, 0))
_check(float(objects[1]["pos"][1]) == 3.0, "Pfeiltaste: Terrain-Abstand bleibt (3 m über flachem Boden)")
_check(float(objects[1]["pos"][0]) == 12.0, "Pfeiltaste: 2 m in +X")
# Mehrfachauswahl wandert gemeinsam, relative Anordnung exakt erhalten.
interaction.select_many([0, 1])
var d0: Array = [float(objects[0]["pos"][0]) - float(objects[1]["pos"][0]),
float(objects[0]["pos"][2]) - float(objects[1]["pos"][2])]
interaction.nudge_selection(Vector3(-2, 0, 0))
var d1: Array = [float(objects[0]["pos"][0]) - float(objects[1]["pos"][0]),
float(objects[0]["pos"][2]) - float(objects[1]["pos"][2])]
_check(absf(d0[0] - d1[0]) < 0.0001 and absf(d0[1] - d1[1]) < 0.0001,
"Mehrfachauswahl: relative Anordnung byte-genau erhalten")
# Map-Rand: außerhalb klemmt — komplett geklemmter Druck committet NICHT.
var small := {"terrain": {"size_x_m": 100.0, "size_z_m": 100.0, "resolution": 0},
"objects": [{"id": "crate_wood", "pos": [50.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0}]}
var pair2 := _make_interaction(small, 1)
var fake2: ProbeMain = pair2[0]
var interaction2 = pair2[1]
var sobjects: Array = small["objects"]
interaction2.select_object(0)
var had_undo: bool = fake2.history.can_undo()
interaction2.nudge_selection(Vector3(2, 0, 0))
_check(float(sobjects[0]["pos"][0]) == 50.0 and fake2.history.can_undo() == had_undo,
"Map-Rand: komplett geklemmte Zelle → keine Bewegung, KEIN History-Eintrag")
# ------------------------------------------- Bild↑/↓ Mehrfach + Boden-Snap ----
func _test_lift_multi() -> void:
print("Testgruppe: Bild↑/↓ auf der GANZEN Auswahl (EIN Undo-Schritt)")
var doc := {"objects": [
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
{"id": "tree_pine", "pos": [20.0, 0.5, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
]}
var pair := _make_interaction(doc, 2)
var interaction = pair[1]
var objects: Array = doc["objects"]
interaction.select_many([0, 1])
interaction.lift_selected(ObjectOps.LIFT_STEP_M)
_check(absf(float(objects[0]["pos"][1]) - 0.25) < 0.0001,
"Bild↑: Objekt 1 +0,25 m")
_check(absf(float(objects[1]["pos"][1]) - 0.75) < 0.0001,
"Bild↑: Objekt 2 +0,25 m (0,5 → 0,75)")
var marker_y: float = (pair[2].object_markers[0] as Node3D).position.y
_check(absf(marker_y - 0.25) < 0.0001, "Bild↑: Marker wandert mit")
interaction.lift_selected(-ObjectOps.LIFT_STEP_M)
_check(absf(float(objects[0]["pos"][1])) < 0.0001, "Bild↓: wieder −0,25 m")
# Objekt am Boden: weiter absenken geht nicht — kein Commit, Meldung kommt.
var can_undo: bool = (pair[0].history as ActionHistory).can_undo()
interaction.lift_selected(-ObjectOps.LIFT_STEP_M)
_check(absf(float(objects[0]["pos"][1])) < 0.0001, "Bild↓ am Boden: y bleibt 0")
_check((pair[0].history as ActionHistory).can_undo() == can_undo,
"Bild↓ am Boden: nichts committet (Snapshot verworfen)")
func _test_snap_to_floor() -> void:
print("Testgruppe: Strg+Bild↑ — Auswahl auf den Boden setzen")
var doc := {"objects": [
{"id": "crate_wood", "pos": [10.0, 3.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
{"id": "tree_pine", "pos": [20.0, 0.0, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
]}
var pair := _make_interaction(doc, 2)
var fake: ProbeMain = pair[0]
var interaction = pair[1]
var objects: Array = doc["objects"]
interaction.select_many([0, 1])
interaction.snap_selection_to_floor()
_check(float(objects[0]["pos"][1]) == 0.0, "Strg+Bild↑: schwebendes Objekt auf Terrain-Höhe (0 m)")
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][2]) == 10.0,
"Strg+Bild↑: X/Z bleiben unangetastet")
_check(fake.history.can_undo(), "Strg+Bild↑: EIN History-Eintrag")
# Zweiter Druck: alles steht schon am Boden → keine Aktion, kein Eintrag.
var can_undo: bool = fake.history.can_undo()
interaction.snap_selection_to_floor()
_check(fake.history.can_undo() == can_undo, "Strg+Bild↑ am Boden: kein leerer Commit")
# --------------------------------------------------------------- Greifen (G) --
func _test_grab() -> void:
print("Testgruppe: G — Greifen mit Esc-Rückabwicklung (byte-genau)")
var doc := {"objects": [
{"id": "crate_wood", "pos": [10.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
{"id": "tree_pine", "pos": [12.0, 0.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
]}
var pair := _make_interaction(doc, 2)
var fake: ProbeMain = pair[0]
var interaction = pair[1]
var builder: ProbeBuilder = pair[2]
var objects: Array = doc["objects"]
fake.grid_snap = true
interaction.select_many([0, 1])
var ok: bool = interaction.grab_begin()
_check(ok, "G mit Auswahl: Greifen startet")
# Linksklick-Setzen: Primär (letzter Auswahl-Index = 1) rastet auf ganze
# Meter, das andere Auswahl-Objekt wandert exakt mit demselben Delta mit.
interaction.grab_move(Vector3(20.3, 0.0, 8.6))
interaction.grab_confirm()
_check(float(objects[1]["pos"][0]) == 20.0 and float(objects[1]["pos"][2]) == 9.0,
"Greifen + LMB: Primär rastet aufs Raster (20, 9)")
_check(float(objects[0]["pos"][0]) == 18.0 and float(objects[0]["pos"][2]) == 9.0,
"Greifen: zweites Auswahl-Objekt wandert exakt mit (+8, −1)")
_check(float(objects[1]["pos"][1]) == 0.0, "Greifen: Füßpunkt auf Terrain-Höhe")
_check(fake.history.can_undo(), "Greifen: EIN History-Eintrag für den ganzen Zug")
fake.history.undo()
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[1]["pos"][0]) == 12.0,
"Strg+Z: Greifen-Zug komplett zurück")
# Esc-Rückabwicklung: Positionen byte-identisch zum Zustand vor dem Greifen.
interaction.grab_begin()
interaction.grab_move(Vector3(30.7, 0.0, 1.2))
_check(float(objects[1]["pos"][0]) == 31.0, "Greifen: live am Cursor (rastet)")
var can_undo: bool = fake.history.can_undo()
interaction.grab_cancel()
_check(float(objects[0]["pos"][0]) == 10.0 and float(objects[0]["pos"][1]) == 0.0
and float(objects[0]["pos"][2]) == 10.0,
"Esc: exakte Rückabwicklung auf die Ausgangsposition (byte-identisch)")
_check(float(objects[1]["pos"][0]) == 12.0 and float(objects[1]["pos"][2]) == 10.0,
"Esc: auch das zweite Objekt zurück")
_check(fake.history.can_undo() == can_undo, "Esc: KEIN History-Eintrag beim Abbruch")
_check(not bool(interaction.get("_grabbing")), "Esc: Greifen-Zustand beendet")
# Marker sind nach dem Abbruch wieder synchron (Viewport = Doc).
_check(absf((builder.object_markers[0] as Node3D).position.x - 10.0) < 0.0001,
"Esc: Viewport-Marker zurückgesetzt")
# G ohne Auswahl: hilfreiche Meldung, kein Crash.
interaction.clear_selection(true)
_check(not interaction.grab_begin(), "G ohne Auswahl: Greifen startet nicht")
_check(fake.statuses.size() > 0, "G ohne Auswahl: hilfreiche Meldung statt Stille")
# ------------------------------------------- Bauen: Pfeile + Strg+Bild↑ -------
func _test_build_nudge_floor() -> void:
print("Testgruppe: Bauen-Modus — Pfeiltasten (2-m-Raster) + Strg+Bild↑")
var doc := {"gebaeude": [
{"id": "wand", "pos": [4.0, 0.0, 6.0], "rot": 0.0, "punkte": [[0, 0, 0], [2, 0, 0]]},
]}
var bfake := ProbeMain.new()
bfake.doc = doc
bfake.history.begin(bfake.doc)
var bbuilder := ProbeBuilder.new()
root.add_child(bbuilder)
var btool := Node3D.new()
btool.set_script(load("res://src/editor/build_tool.gd"))
root.add_child(btool)
btool.setup(bfake, Camera3D.new(), bbuilder, null)
btool.select_index(0)
btool.nudge_selected(Vector3(0, 0, -2))
var list: Array = BuildOps.pieces(doc)
_check(float(list[0]["pos"][2]) == 4.0 and float(list[0]["pos"][0]) == 4.0,
"Bauen-Pfeil: exakt 2 m, Raster-Ausrichtung bleibt")
_check(bfake.history.can_undo(), "Bauen-Pfeil: EIN History-Eintrag")
bfake.history.undo()
_check(float(list[0]["pos"][2]) == 6.0, "Bauen-Pfeil: Strg+Z zurück")
# Strg+Bild↑: Bauteil auf Terrain-Höhe (flaches Terrain → 0 m, 5-cm-Raster).
list[0]["pos"] = [4.0, 2.5, 6.0]
btool.snap_selected_to_floor()
_check(absf(float(list[0]["pos"][1])) < 0.0001, "Bauen Strg+Bild↑: Bauteil auf dem Boden (0 m)")
# Erneut: liegt schon am Boden → Meldung, kein Commit.
var can_undo: bool = bfake.history.can_undo()
btool.snap_selected_to_floor()
_check(bfake.history.can_undo() == can_undo, "Bauen Strg+Bild↑ am Boden: kein leerer Commit")
# ------------------------------------------ Terrain-Ziffer 4 = Malen (Fix) ----
func _test_brush_paint_mode() -> void:
print("Testgruppe: Terrain-Pinsel — Modus 3 (Malen) kommt durch set_brush_mode")
var tool := Node3D.new()
tool.set_script(load("res://src/editor/terrain_tool.gd"))
root.add_child(tool)
tool.call("set_brush_mode", TerrainField.Mode.PAINT)
_check(int(tool.get("brush_mode")) == TerrainField.Mode.PAINT,
"set_brush_mode(Malen) bleibt Malen (alter Clamp 0..2 degradierte still zu Glätten)")
tool.call("set_brush_mode", TerrainField.Mode.RAISE)
_check(int(tool.get("brush_mode")) == TerrainField.Mode.RAISE, "set_brush_mode(Heben) bleibt Heben")
tool.call("set_brush_mode", 99)
_check(int(tool.get("brush_mode")) == TerrainField.Mode.PAINT, "Out-of-Range clampet auf den höchsten Modus")

View file

@ -0,0 +1,20 @@
#!/bin/bash
# Parse-Check der Shortcuts-2-Karte (t_55257792): alle geänderten/neuen Scripts.
cd "$(dirname "$0")/.."
GODOT=/opt/data/home/bin/godot4
fail=0
for f in src/ui/main.gd src/viewport/orbit_camera.gd src/editor/shortcut_keys.gd \
src/editor/move_math.gd src/editor/shortcut_policy.gd \
src/editor/interaction.gd src/editor/object_ops.gd \
src/editor/build_tool.gd src/editor/terrain_tool.gd \
tests/test_shortcuts.gd tests/test_gui_smoke.gd; do
out=$("$GODOT" --headless --path . --check-only --script "res://$f" 2>&1 | grep -iv "godot engine\|^$" || true)
if [ -n "$out" ]; then
echo "FAIL $f:"
echo "$out"
fail=1
else
echo "OK $f"
fi
done
exit $fail

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_spawns test_testrun test_bauen; do for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_spawns test_testrun test_bauen test_gui_smoke; 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)