Map Builder Inspektor Teil 2 (t_e0120dc2): Achsen-Gizmo — drei farbige Pfeile (X rot / Y gruen / Z blau) am einzeln markierten Objekt, ziehbar; Zieh-Werte rasten auf die Schrittweite (Mausrad waehrend des Zugs blaettert fein 0.1 / normal 0.5 / grob 2.0, SpinBox 'Schrittweite' im rechten Panel, ueberlebt UI-Rebuilds); Inspektor-Zahlen folgen live; Y-Zug klemmt am Terrain, X/Z folgen dem Boden; Esc rollt exakt auf die Startposition zurueck (kein History-Eintrag), Loslassen committet EINEN Undo-Schritt; nur Einzelauswahl. Neues src/viewport/gizmo.gd haengt als Kind von interaction.gd (Input-Prioritaet), interaction.gd unveraendert. OPERATOR-NACHTRAG: dieser Stand lag nur als Karten-Anhang vor (nie committet) — vom Operator uebernommen und verifiziert: --import 0 Parse-Fehler, test_gui_smoke 89 OK, test_inspector 33 OK, test_ui_scale 51/0, test_library 101 OK.
This commit is contained in:
parent
9fa8029910
commit
e664d09666
3 changed files with 479 additions and 0 deletions
|
|
@ -34,6 +34,9 @@ var grid_snap := false # Raster-Snap (1 m) an/aus — Objekt-Werkzeug
|
||||||
|
|
||||||
var history := ActionHistory.new()
|
var history := ActionHistory.new()
|
||||||
var interaction # src/editor/interaction.gd (script-erweitert → untypisiert)
|
var interaction # src/editor/interaction.gd (script-erweitert → untypisiert)
|
||||||
|
var gizmo # src/viewport/gizmo.gd (Achsen-Pfeile, Kind von interaction)
|
||||||
|
var gizmo_step_m := 0.5 # Gizmo-Schrittweite in m (Panel-Feld; überlebt UI-Rebuilds)
|
||||||
|
var gizmo_step_box: SpinBox # SpinBox „Schrittweite“ im rechten Panel
|
||||||
var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1)
|
var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1)
|
||||||
var spawn_tool # src/editor/spawn_tool.gd (Zonen, Karte t_c4162c5d)
|
var spawn_tool # src/editor/spawn_tool.gd (Zonen, Karte t_c4162c5d)
|
||||||
var build_tool # src/editor/build_tool.gd (Bauen-Modus, Karte t_97ea59c5)
|
var build_tool # src/editor/build_tool.gd (Bauen-Modus, Karte t_97ea59c5)
|
||||||
|
|
@ -62,6 +65,7 @@ const LIBRARY_CFG_SECTION := "library" # Abschnitt in user://scopedvr_editor.c
|
||||||
# Objekt-Inspektor (Karte t_bb0d11c9) — preload statt class_name, damit die
|
# Objekt-Inspektor (Karte t_bb0d11c9) — preload statt class_name, damit die
|
||||||
# Referenz auch ohne frischen Import-Lauf auflöst (Belt-and-braces).
|
# Referenz auch ohne frischen Import-Lauf auflöst (Belt-and-braces).
|
||||||
const INSPECTOR_PANEL := preload("res://src/ui/inspector_panel.gd")
|
const INSPECTOR_PANEL := preload("res://src/ui/inspector_panel.gd")
|
||||||
|
const GizmoScript := preload("res://src/viewport/gizmo.gd")
|
||||||
|
|
||||||
var undo_btn: Button
|
var undo_btn: Button
|
||||||
var redo_btn: Button
|
var redo_btn: Button
|
||||||
|
|
@ -782,6 +786,12 @@ func _build_center_viewport() -> SubViewportContainer:
|
||||||
interaction.set_script(load("res://src/editor/interaction.gd"))
|
interaction.set_script(load("res://src/editor/interaction.gd"))
|
||||||
world.add_child(interaction)
|
world.add_child(interaction)
|
||||||
interaction.setup(self, camera, view_builder)
|
interaction.setup(self, camera, view_builder)
|
||||||
|
# Achsen-Gizmo (Karte t_e0120dc2) als KIND von interaction — Kinder
|
||||||
|
# bekommen _unhandled_input ZUERST, es fängt Pfeil-Klicks ab bevor
|
||||||
|
# interaction sie sieht. interaction.gd bleibt dadurch unverändert.
|
||||||
|
gizmo = GizmoScript.new()
|
||||||
|
gizmo.setup(self, camera, view_builder)
|
||||||
|
interaction.add_child(gizmo)
|
||||||
camera.set("wheel_filter", Callable(interaction, "wheel_blocked"))
|
camera.set("wheel_filter", Callable(interaction, "wheel_blocked"))
|
||||||
# Freelook-Flug-Verdrahtung (Karte t_c46d31c2): Status-Meldungen der
|
# Freelook-Flug-Verdrahtung (Karte t_c46d31c2): Status-Meldungen der
|
||||||
# Kamera laufen ins Statusfeld. Karte t_2470de89: die rechte Maustaste
|
# Kamera laufen ins Statusfeld. Karte t_2470de89: die rechte Maustaste
|
||||||
|
|
@ -953,6 +963,29 @@ func _build_right_panel() -> PanelContainer:
|
||||||
group_btns.add_child(sel_ungroup_btn)
|
group_btns.add_child(sel_ungroup_btn)
|
||||||
vbox.add_child(_muted_label("Mehrfachauswahl: Umschalt+Klick oder Rahmen aufziehen. Eine Gruppe wandert und dreht gemeinsam — auch nach Speichern/Laden."))
|
vbox.add_child(_muted_label("Mehrfachauswahl: Umschalt+Klick oder Rahmen aufziehen. Eine Gruppe wandert und dreht gemeinsam — auch nach Speichern/Laden."))
|
||||||
|
|
||||||
|
# --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen-
|
||||||
|
# Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die
|
||||||
|
# Voreinstellungen (fein/normal/grob); dieses Feld setzt sie exakt und
|
||||||
|
# überlebt UI-Rebuilds, weil der Wert in gizmo_step_m liegen bleibt.
|
||||||
|
vbox.add_child(_header_label("Gizmo-Einstellungen"))
|
||||||
|
var gizmo_row := HBoxContainer.new()
|
||||||
|
gizmo_row.add_theme_constant_override("separation", UIScale.px(8))
|
||||||
|
vbox.add_child(gizmo_row)
|
||||||
|
gizmo_row.add_child(_field_label("Schrittweite"))
|
||||||
|
gizmo_step_box = SpinBox.new()
|
||||||
|
gizmo_step_box.min_value = 0.01
|
||||||
|
gizmo_step_box.max_value = 10.0
|
||||||
|
gizmo_step_box.step = 0.01
|
||||||
|
gizmo_step_box.value = gizmo_step_m
|
||||||
|
gizmo_step_box.custom_minimum_size = UIScale.v2(110, 30)
|
||||||
|
gizmo_step_box.tooltip_text = "Wie weit bewegt sich das Objekt am Achsen-Gizmo pro Schritt (in Metern). Mausrad WÄHREND des Ziehens wechselt zwischen fein (0.1) / normal (0.5) / grob (2.0)."
|
||||||
|
gizmo_row.add_child(gizmo_step_box)
|
||||||
|
gizmo_step_box.value_changed.connect(func(v: float) -> void:
|
||||||
|
gizmo_step_m = v
|
||||||
|
if gizmo != null and is_instance_valid(gizmo):
|
||||||
|
gizmo.set_step_m(v)
|
||||||
|
)
|
||||||
|
|
||||||
vbox.add_child(_separator())
|
vbox.add_child(_separator())
|
||||||
|
|
||||||
# --- Map-Eigenschaften ---
|
# --- Map-Eigenschaften ---
|
||||||
|
|
@ -1973,6 +2006,12 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
_toggle_help_overlay()
|
_toggle_help_overlay()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
# Karte t_e0120dc2: Läuft gerade ein Gizmo-Zug (Achsen-Pfeil), gehört
|
||||||
|
# Esc IHM — exakt zurückrollen auf die Zug-Startposition (kein
|
||||||
|
# History-Eintrag), danach NICHT weiter in der Esc-Kette eintauchen.
|
||||||
|
if gizmo != null and gizmo.cancel_drag_if_active():
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
# Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar):
|
# Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar):
|
||||||
# Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview
|
# Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview
|
||||||
# abbrechen → Auswahl aufheben (Greifen: Karte t_55257792).
|
# abbrechen → Auswahl aufheben (Greifen: Karte t_55257792).
|
||||||
|
|
@ -2615,6 +2654,12 @@ func _sync_selection_panel() -> void:
|
||||||
# Objekt-Inspektor mit allen Auswahl-Wege synchron halten (Karte t_bb0d11c9).
|
# Objekt-Inspektor mit allen Auswahl-Wege synchron halten (Karte t_bb0d11c9).
|
||||||
if inspector != null:
|
if inspector != null:
|
||||||
inspector.call("refresh")
|
inspector.call("refresh")
|
||||||
|
# Gizmo-Schrittweite-Feld mitziehen (Karte t_e0120dc2): Das Mausrad
|
||||||
|
# WÄHREND eines Zugs kann sie im Gizmo ändern (cycle_step) — das Panel-
|
||||||
|
# Feld folgt ohne eigene value_changed-Rückkopplung (set_value_no_signal).
|
||||||
|
if gizmo_step_box != null and is_instance_valid(gizmo_step_box) \
|
||||||
|
and not gizmo_step_box.has_focus():
|
||||||
|
gizmo_step_box.set_value_no_signal(gizmo_step_m)
|
||||||
var objects: Array = doc.get("objects", [])
|
var objects: Array = doc.get("objects", [])
|
||||||
var sel: Array = interaction.selected_indices
|
var sel: Array = interaction.selected_indices
|
||||||
var valid: Array = []
|
var valid: Array = []
|
||||||
|
|
|
||||||
433
src/viewport/gizmo.gd
Normal file
433
src/viewport/gizmo.gd
Normal file
|
|
@ -0,0 +1,433 @@
|
||||||
|
extends Node3D
|
||||||
|
## Achsen-Gizmo (Karte t_e0120dc2): drei farbige Pfeile (X rot, Y grün,
|
||||||
|
## Z blau) am EINZELN markierten Objekt. Zieht man einen Pfeil, bewegt sich
|
||||||
|
## das Objekt GENAU entlang dieser Achse — die Zahlen im Objekt-Inspektor
|
||||||
|
## laufen LIVE mit. Mausrad WÄHREND des Ziehens wechselt die Schrittweite
|
||||||
|
## (fein/normal/grob); derselbe Wert steht als „Schrittweite“-Feld im
|
||||||
|
## rechten Panel (main.gd).
|
||||||
|
##
|
||||||
|
## Regeln der Karte:
|
||||||
|
## - NUR Einzelauswahl (Mehrfachauswahl bekommt kein Gizmo — Folgekarte).
|
||||||
|
## - JEDER Zug = EIN Undo-Schritt (Commit beim Loslassen). Esc bricht EXAKT
|
||||||
|
## auf die Startposition zurück (byte-identisch, wie grab_cancel).
|
||||||
|
## - Zieh-Werte rasten auf Vielfache der Schrittweite (Voreinstellungen
|
||||||
|
## 0,1 / 0,5 / 2 m; das Feld im Panel nimmt jeden Wert 0,01–10 m).
|
||||||
|
## - Klemmen wie der Inspektor: X/Z an der Map-Grenze, Y nie unter dem
|
||||||
|
## Terrain (X/Z-Zug folgt dem Boden wie der normale Objekt-Zug) und
|
||||||
|
## oben an der Höhen-Grenze.
|
||||||
|
##
|
||||||
|
## Eingabe-Verdrahtung: dieses Gizmo hängt als KIND an interaction.gd —
|
||||||
|
## Knoten bekommen _unhandled_input TIEFER IM BAUM ZUERST, so fängt es
|
||||||
|
## Pfeil-Klicks ab, BEVOR die Auswahl-/Zieh-Logik des Parents sie sieht.
|
||||||
|
## interaction.gd bleibt dadurch UNVERÄNDERT (Karten-Grenze) und der
|
||||||
|
## bestehende Klick-/Zieh-Pfad wird 1:1 wiederverwendet.
|
||||||
|
|
||||||
|
const SvmapDocument := preload("res://src/svmap/map_document.gd")
|
||||||
|
|
||||||
|
const AXIS_LEN := 2.2 # Pfeil-Länge in Metern (Weltmaß)
|
||||||
|
const SHAFT_R := 0.045
|
||||||
|
const CONE_H := 0.45
|
||||||
|
const CONE_R := 0.13
|
||||||
|
const HIT_PX := 18.0 # Treffer-Toleranz am Bildschirm (Pixel)
|
||||||
|
const MIN_PX_PER_M := 10.0 # Zieh-Sensitivität deckeln (Achse fast im Blick)
|
||||||
|
const STEP_MIN := 0.01
|
||||||
|
const STEP_MAX := 10.0
|
||||||
|
|
||||||
|
const STEP_PRESETS := [0.1, 0.5, 2.0]
|
||||||
|
const STEP_PRESET_NAMES := ["fein", "normal", "grob"]
|
||||||
|
|
||||||
|
const AXES := {
|
||||||
|
"x": Vector3(1.0, 0.0, 0.0),
|
||||||
|
"y": Vector3(0.0, 1.0, 0.0),
|
||||||
|
"z": Vector3(0.0, 0.0, 1.0),
|
||||||
|
}
|
||||||
|
const AXIS_COLORS := {
|
||||||
|
"x": Color(1.0, 0.25, 0.25),
|
||||||
|
"y": Color(0.3, 0.95, 0.35),
|
||||||
|
"z": Color(0.35, 0.55, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
var main # src/ui/main.gd (script-erweitert)
|
||||||
|
var camera: Camera3D
|
||||||
|
var view_builder # src/viewport/map_view_builder.gd
|
||||||
|
|
||||||
|
var step_m := 0.5 # Schrittweite (m) — Panel-Feld + Mausrad-Presets
|
||||||
|
|
||||||
|
# --- Zieh-Zustand ---
|
||||||
|
var _dragging := false
|
||||||
|
var _drag_idx := -1
|
||||||
|
var _drag_axis := ""
|
||||||
|
var _drag_axis_vec := Vector3.ZERO
|
||||||
|
var _press_screen := Vector2.ZERO
|
||||||
|
var _screen_axis := Vector2.RIGHT # Achse am Bildschirm (normalisiert, px)
|
||||||
|
var _px_per_m := 1.0
|
||||||
|
var _start_pos := Vector3.ZERO # doc-Position beim Zieh-Start
|
||||||
|
var _start_saved: Array = [] # exakte Start-Position für Esc-Rückabwicklung
|
||||||
|
var _drag_lift := 0.0 # Boden-Abstand (X/Z-Zug folgt dem Terrain)
|
||||||
|
var _drag_moved := false
|
||||||
|
var _last_delta := 0.0 # zuletzt angewendeter Delta (m) — 0 = kein Commit
|
||||||
|
var _drag_before: Dictionary = {} # History-Snapshot VOR dem Zug
|
||||||
|
|
||||||
|
var _arrows := {} # "x"/"y"/"z" → Node3D (Pfeil-Root)
|
||||||
|
|
||||||
|
|
||||||
|
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
||||||
|
main = p_main
|
||||||
|
camera = p_camera
|
||||||
|
view_builder = p_builder
|
||||||
|
_build_arrows()
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
|
||||||
|
## Pfeile bauen: Schaft (Zylinder) + Spitze (Kegel), ungeschattet und OHNE
|
||||||
|
## Tiefentest — die Pfeile bleiben auch vor/über Objekten gut sichtbar.
|
||||||
|
func _build_arrows() -> void:
|
||||||
|
for a in AXES:
|
||||||
|
var root := Node3D.new()
|
||||||
|
# Alle Pfeile zeigen lokal +Y; der Root dreht sie auf die Welt-Achse.
|
||||||
|
if a == "x":
|
||||||
|
root.rotation.z = -PI / 2.0 # +Y → +X
|
||||||
|
elif a == "z":
|
||||||
|
root.rotation.x = PI / 2.0 # +Y → +Z
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = AXIS_COLORS[a]
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
mat.no_depth_test = true
|
||||||
|
mat.render_priority = 20
|
||||||
|
var shaft := MeshInstance3D.new()
|
||||||
|
var cyl := CylinderMesh.new()
|
||||||
|
cyl.top_radius = SHAFT_R
|
||||||
|
cyl.bottom_radius = SHAFT_R
|
||||||
|
cyl.height = AXIS_LEN
|
||||||
|
shaft.mesh = cyl
|
||||||
|
shaft.material_override = mat
|
||||||
|
shaft.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
shaft.position.y = AXIS_LEN * 0.5
|
||||||
|
root.add_child(shaft)
|
||||||
|
var tip := MeshInstance3D.new()
|
||||||
|
var cone := CylinderMesh.new()
|
||||||
|
cone.top_radius = 0.0
|
||||||
|
cone.bottom_radius = CONE_R
|
||||||
|
cone.height = CONE_H
|
||||||
|
tip.mesh = cone
|
||||||
|
tip.material_override = mat
|
||||||
|
tip.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
tip.position.y = AXIS_LEN + CONE_H * 0.5
|
||||||
|
root.add_child(tip)
|
||||||
|
add_child(root)
|
||||||
|
_arrows[a] = root
|
||||||
|
|
||||||
|
|
||||||
|
func _axis_vec(a: String) -> Vector3:
|
||||||
|
return AXES.get(a, Vector3.ZERO) as Vector3
|
||||||
|
|
||||||
|
|
||||||
|
## Sichtbarkeit + Position JEDES Frames aus der Auswahl ableiten (kein
|
||||||
|
## Verdrahten jedes Auswahl-Wegs nötig — Auswahl kommt aus interaction.gd,
|
||||||
|
## Baum, Undo/Redo, Inspektor … alles landet automatisch hier).
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if main == null or view_builder == null:
|
||||||
|
return
|
||||||
|
var inter = main.get("interaction")
|
||||||
|
if inter == null:
|
||||||
|
return
|
||||||
|
var sel: Array = inter.selected_indices
|
||||||
|
if sel.size() != 1:
|
||||||
|
visible = false
|
||||||
|
return
|
||||||
|
if bool(inter.get("placing")):
|
||||||
|
visible = false # Ghost-Platzieren: keine Pfeile im Weg
|
||||||
|
return
|
||||||
|
var idx: int = int(inter.selected_index)
|
||||||
|
var markers: Array = view_builder.object_markers
|
||||||
|
if idx < 0 or idx >= markers.size():
|
||||||
|
visible = false
|
||||||
|
return
|
||||||
|
var marker = markers[idx]
|
||||||
|
if marker == null or not is_instance_valid(marker):
|
||||||
|
visible = false
|
||||||
|
return
|
||||||
|
visible = true
|
||||||
|
global_position = marker.global_position
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if main == null or camera == null:
|
||||||
|
return
|
||||||
|
var doc: Dictionary = main.get("doc")
|
||||||
|
if doc == null or doc.is_empty():
|
||||||
|
return
|
||||||
|
var mode: String = str(main.get("tool_mode"))
|
||||||
|
if mode == "terrain" or mode == "spawns":
|
||||||
|
return # Terrain-Pinsel / Spawn-Zonen haben das Zepter
|
||||||
|
if event is InputEventMouseButton:
|
||||||
|
var mb := event as InputEventMouseButton
|
||||||
|
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
|
||||||
|
if _dragging:
|
||||||
|
return
|
||||||
|
var inter = main.get("interaction")
|
||||||
|
if inter == null or bool(inter.get("placing")):
|
||||||
|
return
|
||||||
|
var axis := pick(mb.position)
|
||||||
|
if axis != "":
|
||||||
|
_begin_drag(axis, mb.position)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed:
|
||||||
|
if _dragging:
|
||||||
|
_finish_drag(true)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif (mb.button_index == MOUSE_BUTTON_WHEEL_UP \
|
||||||
|
or mb.button_index == MOUSE_BUTTON_WHEEL_DOWN) and mb.pressed:
|
||||||
|
if _dragging:
|
||||||
|
var dir := 1 if mb.button_index == MOUSE_BUTTON_WHEEL_UP else -1
|
||||||
|
cycle_step(dir)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event is InputEventMouseMotion and _dragging:
|
||||||
|
_drag_motion((event as InputEventMouseMotion).position)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event is InputEventKey and (event as InputEventKey).pressed \
|
||||||
|
and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||||
|
if _dragging:
|
||||||
|
_finish_drag(false)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Treffer -----
|
||||||
|
|
||||||
|
## Achse unter dem Cursor (Bildschirm-Segment-Distanz) — "" = kein Pfeil.
|
||||||
|
func pick(screen_pos: Vector2) -> String:
|
||||||
|
if not visible:
|
||||||
|
return ""
|
||||||
|
var o := _safe_unproject(global_position)
|
||||||
|
if o == Vector2.INF:
|
||||||
|
return ""
|
||||||
|
var best := ""
|
||||||
|
var best_d := HIT_PX
|
||||||
|
for a in AXES:
|
||||||
|
var t := _safe_unproject(global_position + _axis_vec(a) * AXIS_LEN)
|
||||||
|
if t == Vector2.INF:
|
||||||
|
continue
|
||||||
|
var d := _dist_point_segment(screen_pos, o, t)
|
||||||
|
if d < best_d:
|
||||||
|
best_d = d
|
||||||
|
best = a
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
func _safe_unproject(p: Vector3) -> Vector2:
|
||||||
|
if camera == null:
|
||||||
|
return Vector2.INF
|
||||||
|
if camera.is_position_behind(p):
|
||||||
|
return Vector2.INF
|
||||||
|
return camera.unproject_position(p)
|
||||||
|
|
||||||
|
|
||||||
|
func _dist_point_segment(p: Vector2, a: Vector2, b: Vector2) -> float:
|
||||||
|
var ab := b - a
|
||||||
|
var len_sq := ab.length_squared()
|
||||||
|
if len_sq < 0.0001:
|
||||||
|
return p.distance_to(a)
|
||||||
|
var t := clampf((p - a).dot(ab) / len_sq, 0.0, 1.0)
|
||||||
|
return p.distance_to(a + ab * t)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------- Ziehen -----
|
||||||
|
|
||||||
|
func _begin_drag(axis: String, mb_pos: Vector2) -> void:
|
||||||
|
if not _begin_drag_state(axis):
|
||||||
|
return
|
||||||
|
# Bildschirm-Mathe: Achse in Pixeln → Zieh-Sensitivität (Meter pro Pixel).
|
||||||
|
var markers: Array = view_builder.object_markers
|
||||||
|
var marker = markers[_drag_idx]
|
||||||
|
var origin: Vector3 = marker.global_position
|
||||||
|
var o := _safe_unproject(origin)
|
||||||
|
var t := _safe_unproject(origin + _drag_axis_vec * AXIS_LEN)
|
||||||
|
if o == Vector2.INF or t == Vector2.INF:
|
||||||
|
_dragging = false
|
||||||
|
return
|
||||||
|
var seg := t - o
|
||||||
|
var seg_len := seg.length()
|
||||||
|
if seg_len < 1.0:
|
||||||
|
_dragging = false
|
||||||
|
return
|
||||||
|
_screen_axis = seg.normalized()
|
||||||
|
_px_per_m = maxf(seg_len / AXIS_LEN, MIN_PX_PER_M)
|
||||||
|
_press_screen = mb_pos
|
||||||
|
main.call("_set_status", "Gizmo-Zug: entlang %s ziehen — Mausrad wechselt die Schrittweite (%.2f m), Esc bricht ab." % [
|
||||||
|
_drag_axis.to_upper(), step_m])
|
||||||
|
|
||||||
|
|
||||||
|
## Zieh-Zustand NUR aus dem Doc/Marke r aufsetzen (ohne Bildschirm-Mathe —
|
||||||
|
## auch für den Demo-/Test-Hook demo_drag_axis).
|
||||||
|
func _begin_drag_state(axis: String) -> bool:
|
||||||
|
var inter = main.get("interaction")
|
||||||
|
if inter == null or inter.selected_indices.size() != 1:
|
||||||
|
return false
|
||||||
|
var idx: int = int(inter.selected_index)
|
||||||
|
var markers: Array = view_builder.object_markers
|
||||||
|
if idx < 0 or idx >= markers.size():
|
||||||
|
return false
|
||||||
|
var marker = markers[idx]
|
||||||
|
if marker == null or not is_instance_valid(marker):
|
||||||
|
return false
|
||||||
|
var objects: Array = main.doc.get("objects", [])
|
||||||
|
if idx >= objects.size():
|
||||||
|
return false
|
||||||
|
var pos: Array = (objects[idx] as Dictionary).get("pos", [0.0, 0.0, 0.0])
|
||||||
|
_drag_idx = idx
|
||||||
|
_drag_axis = axis
|
||||||
|
_drag_axis_vec = _axis_vec(axis)
|
||||||
|
_press_screen = Vector2.ZERO
|
||||||
|
_start_pos = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||||||
|
_start_saved = (pos as Array).duplicate()
|
||||||
|
_drag_lift = marker.position.y - SvmapDocument.terrain_height_at(
|
||||||
|
main.doc, _start_pos.x, _start_pos.z)
|
||||||
|
_drag_before = main.history.snapshot(["objects"])
|
||||||
|
_drag_moved = false
|
||||||
|
_last_delta = 0.0
|
||||||
|
_dragging = true
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _drag_motion(mb_pos: Vector2) -> void:
|
||||||
|
var d := mb_pos - _press_screen
|
||||||
|
var raw := d.dot(_screen_axis) / _px_per_m
|
||||||
|
_apply_axis_delta(snappedf(raw, step_m))
|
||||||
|
|
||||||
|
|
||||||
|
## Delta entlang der Achse anwenden: doc + Marke + Gizmo, Inspektor LIVE.
|
||||||
|
func _apply_axis_delta(delta: float) -> void:
|
||||||
|
if not _dragging or absf(delta - _last_delta) < 0.0005:
|
||||||
|
return
|
||||||
|
var objects: Array = main.doc.get("objects", [])
|
||||||
|
if _drag_idx >= objects.size():
|
||||||
|
_finish_drag(false)
|
||||||
|
return
|
||||||
|
var np := _start_pos + _drag_axis_vec * delta
|
||||||
|
var half := _terrain_half()
|
||||||
|
np.x = clampf(np.x, -half.x, half.x)
|
||||||
|
np.z = clampf(np.z, -half.y, half.y)
|
||||||
|
if _drag_axis == "y":
|
||||||
|
np.y = clampf(np.y, SvmapDocument.terrain_height_at(main.doc, np.x, np.z), _max_y())
|
||||||
|
else:
|
||||||
|
# X/Z-Zug folgt dem Boden (gleiches Verhalten wie der normale Zug):
|
||||||
|
# der Boden-Abstand bleibt erhalten, nichts begräbt sich.
|
||||||
|
np.y = SvmapDocument.terrain_height_at(main.doc, np.x, np.z) + _drag_lift
|
||||||
|
var obj: Dictionary = objects[_drag_idx]
|
||||||
|
obj["pos"] = [np.x, np.y, np.z]
|
||||||
|
var markers: Array = view_builder.object_markers
|
||||||
|
if _drag_idx < markers.size():
|
||||||
|
var marker = markers[_drag_idx]
|
||||||
|
if marker != null and is_instance_valid(marker):
|
||||||
|
marker.position = np
|
||||||
|
global_position = np
|
||||||
|
_last_delta = delta
|
||||||
|
_drag_moved = true
|
||||||
|
main.call("_sync_selection_panel") # Inspektor-Zahlen LIVE mitziehen
|
||||||
|
|
||||||
|
|
||||||
|
## Zug beenden: commit = true → EIN Undo-Schritt (wirklich bewegt),
|
||||||
|
## commit = false → exakte Rückabwicklung, KEIN History-Eintrag.
|
||||||
|
func _finish_drag(commit: bool) -> void:
|
||||||
|
if not _dragging:
|
||||||
|
return
|
||||||
|
_dragging = false
|
||||||
|
var objects: Array = main.doc.get("objects", [])
|
||||||
|
var idx := _drag_idx
|
||||||
|
if commit and absf(_last_delta) > 0.0005 and idx >= 0 and idx < objects.size():
|
||||||
|
var desc: String
|
||||||
|
match _drag_axis:
|
||||||
|
"x":
|
||||||
|
desc = "Objekt entlang X verschoben (Gizmo)"
|
||||||
|
"y":
|
||||||
|
desc = "Objekt angehoben/abgesenkt (Gizmo, Y-Achse)"
|
||||||
|
_:
|
||||||
|
desc = "Objekt entlang Z verschoben (Gizmo)"
|
||||||
|
main.history.commit(desc, _drag_before, main.history.snapshot(["objects"]))
|
||||||
|
main.set("dirty", true)
|
||||||
|
main.call("_refresh_file_label")
|
||||||
|
main.call("_refresh_history_buttons")
|
||||||
|
main.call("_refresh_stats")
|
||||||
|
main.call("_sync_selection_panel")
|
||||||
|
main.call("_set_status", "%s — %.2f m. Strg+Z nimmt den Zug zurück; Mausrad WÄHREND des Ziehens wechselt fein/grob." % [
|
||||||
|
desc, absf(_last_delta)])
|
||||||
|
elif not commit and idx >= 0 and idx < objects.size():
|
||||||
|
(objects[idx] as Dictionary)["pos"] = _start_saved.duplicate()
|
||||||
|
var markers: Array = view_builder.object_markers
|
||||||
|
if idx < markers.size():
|
||||||
|
var marker = markers[idx]
|
||||||
|
if marker != null and is_instance_valid(marker):
|
||||||
|
marker.position = _start_pos
|
||||||
|
global_position = _start_pos
|
||||||
|
main.call("_sync_selection_panel")
|
||||||
|
main.call("_set_status", "Gizmo-Zug abgebrochen — Position wie vorher.")
|
||||||
|
_drag_before = {}
|
||||||
|
_drag_idx = -1
|
||||||
|
_drag_axis = ""
|
||||||
|
_drag_moved = false
|
||||||
|
_last_delta = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
## Für die Esc-Kette in main.gd: Zug abbrechen, falls einer läuft. true =
|
||||||
|
## es lief einer und wurde abgebrochen (idempotent, weil _dragging prüft).
|
||||||
|
func cancel_drag_if_active() -> bool:
|
||||||
|
if not _dragging:
|
||||||
|
return false
|
||||||
|
_finish_drag(false)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ Schrittweite -------
|
||||||
|
|
||||||
|
## Mausrad WÄHREND des Ziehens: durch die Voreinstellungen blättern
|
||||||
|
## (fein 0,1 → normal 0,5 → grob 2,0 m, wrappt). Das Panel-Feld folgt.
|
||||||
|
func cycle_step(dir: int) -> void:
|
||||||
|
var idx := STEP_PRESETS.find(step_m)
|
||||||
|
if idx < 0:
|
||||||
|
idx = STEP_PRESETS.size() - 1 # eigener Feld-Wert: vom größten Preset starten
|
||||||
|
idx = wrapi(idx + dir, 0, STEP_PRESETS.size())
|
||||||
|
set_step_m(STEP_PRESETS[idx])
|
||||||
|
main.call("_set_status", "Gizmo-Schrittweite: %.2f m (%s) — Mausrad wechselt weiter, das Feld rechts setzt sie exakt." % [
|
||||||
|
step_m, STEP_PRESET_NAMES[idx]])
|
||||||
|
|
||||||
|
|
||||||
|
## Feld-Wert übernehmen (Panel-Feld ODER Mausrad-Preset). Haupt-Var hält den
|
||||||
|
## Wert über UI-Rebuilds (UIScale) hinweg.
|
||||||
|
func set_step_m(v: float) -> void:
|
||||||
|
step_m = clampf(v, STEP_MIN, STEP_MAX)
|
||||||
|
if main != null:
|
||||||
|
main.set("gizmo_step_m", step_m)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- Demo-/Test-Hook ---
|
||||||
|
|
||||||
|
## Probes/Tests: denselben Anwendungs- + Commit-Pfad wie ein echter Zug,
|
||||||
|
## ohne Bildschirm-Mathe. true = es wurde wirklich bewegt + committed.
|
||||||
|
func demo_drag_axis(axis: String, meters: float) -> bool:
|
||||||
|
if main == null:
|
||||||
|
return false
|
||||||
|
var applied := snappedf(meters, step_m)
|
||||||
|
if absf(applied) < 0.0005:
|
||||||
|
return false
|
||||||
|
if not _begin_drag_state(axis):
|
||||||
|
return false
|
||||||
|
_apply_axis_delta(applied)
|
||||||
|
_finish_drag(true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Helfer -------
|
||||||
|
|
||||||
|
func _terrain_half() -> Vector2:
|
||||||
|
var terrain: Dictionary = main.doc.get("terrain", {})
|
||||||
|
return Vector2(float(terrain.get("size_x_m", 1024.0)),
|
||||||
|
float(terrain.get("size_z_m", 1024.0))) * 0.5
|
||||||
|
|
||||||
|
|
||||||
|
func _max_y() -> float:
|
||||||
|
var bounds: Dictionary = main.doc.get("bounds", {})
|
||||||
|
var mx = bounds.get("max", null)
|
||||||
|
if mx is Array and (mx as Array).size() == 3:
|
||||||
|
return float((mx as Array)[1])
|
||||||
|
return 80.0
|
||||||
1
src/viewport/gizmo.gd.uid
Normal file
1
src/viewport/gizmo.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bchu6tv6aetfh
|
||||||
Loading…
Reference in a new issue