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:
parent
095af5ff3c
commit
08a75ac430
15 changed files with 1081 additions and 49 deletions
|
|
@ -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.)*
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
61
src/editor/move_math.gd
Normal 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ß
|
||||||
1
src/editor/move_math.gd.uid
Normal file
1
src/editor/move_math.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://behfbtvkyai65
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
132
src/ui/main.gd
132
src/ui/main.gd
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
20
tools/parse_check_shortcuts2.sh
Normal file
20
tools/parse_check_shortcuts2.sh
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue