624 lines
21 KiB
GDScript
624 lines
21 KiB
GDScript
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://core/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)
|
||
|
||
# --- Dreh-/Kipp-Modus (25.10.): Ringe um alle drei Achsen -------------------
|
||
const RING_R := 1.5 # Ring-Radius in Metern
|
||
const RING_SEGMENTS := 48
|
||
const ROT_STEP_DEG := 15.0
|
||
var mode := "move" # "move" = Pfeile, "rotate" = Ringe
|
||
var rot_step_deg := ROT_STEP_DEG
|
||
var _rings := {} # "x"/"y"/"z" → MeshInstance3D
|
||
var _rot_dragging := false
|
||
var _rot_axis := ""
|
||
var _rot_center := Vector2.ZERO
|
||
var _rot_start_angle := 0.0
|
||
var _rot_applied := 0.0 # bisher angewendete (gerastete) Grad
|
||
var _rot_suffix := "" # merge-Kennung: EIN Undo-Schritt pro Zug
|
||
var _rot_serial := 0
|
||
|
||
|
||
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
||
main = p_main
|
||
camera = p_camera
|
||
view_builder = p_builder
|
||
_build_arrows()
|
||
_build_rings()
|
||
_apply_mode_visibility()
|
||
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
|
||
|
||
|
||
## Drehringe bauen: je Achse ein dünner Torus, ungeschattet, ohne Tiefentest.
|
||
func _build_rings() -> void:
|
||
for a in AXES:
|
||
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 ring := MeshInstance3D.new()
|
||
var torus := TorusMesh.new()
|
||
torus.inner_radius = RING_R - 0.06
|
||
torus.outer_radius = RING_R + 0.06
|
||
torus.rings = 48
|
||
torus.ring_segments = 8
|
||
ring.mesh = torus
|
||
ring.material_override = mat
|
||
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
# Torus liegt lokal in der XZ-Ebene (Achse = Y).
|
||
if a == "x":
|
||
ring.rotation.z = PI / 2.0
|
||
elif a == "z":
|
||
ring.rotation.x = PI / 2.0
|
||
ring.visible = false
|
||
add_child(ring)
|
||
_rings[a] = ring
|
||
|
||
|
||
## Modus wechseln ("move" = Verschieben, "rotate" = Drehen/Kippen).
|
||
func set_mode(m: String) -> void:
|
||
if m != "move" and m != "rotate":
|
||
return
|
||
if _dragging:
|
||
_finish_drag(false)
|
||
if _rot_dragging:
|
||
_finish_rotate(false)
|
||
mode = m
|
||
var inter = main.get("interaction") if main != null else null
|
||
if inter != null:
|
||
inter.no_drag_move = (m == "rotate")
|
||
_apply_mode_visibility()
|
||
|
||
|
||
func _apply_mode_visibility() -> void:
|
||
for a in _arrows:
|
||
(_arrows[a] as Node3D).visible = mode == "move"
|
||
for a in _rings:
|
||
(_rings[a] as Node3D).visible = mode == "rotate"
|
||
|
||
|
||
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 tool_mode: String = str(main.get("tool_mode"))
|
||
if tool_mode == "terrain" or tool_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
|
||
if mode == "rotate":
|
||
var ring_axis := pick_ring(mb.position)
|
||
if ring_axis != "":
|
||
_begin_rotate(ring_axis, mb.position)
|
||
get_viewport().set_input_as_handled()
|
||
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 _rot_dragging:
|
||
_finish_rotate(true)
|
||
get_viewport().set_input_as_handled()
|
||
elif _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 _rot_dragging:
|
||
get_viewport().set_input_as_handled() # Mausrad zoomt nicht mitten im Dreh-Zug
|
||
elif _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 _rot_dragging:
|
||
_rotate_motion((event as InputEventMouseMotion).position)
|
||
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 _rot_dragging:
|
||
_finish_rotate(false)
|
||
get_viewport().set_input_as_handled()
|
||
elif _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
|
||
|
||
|
||
## Ring unter dem Cursor (Abstand zum projizierten Kreis) — "" = keiner.
|
||
func pick_ring(screen_pos: Vector2) -> String:
|
||
if not visible or mode != "rotate":
|
||
return ""
|
||
var best := ""
|
||
var best_d := HIT_PX * 1.6
|
||
for a in AXES:
|
||
var pts := _ring_screen_points(a)
|
||
for i in pts.size() - 1:
|
||
if pts[i] == Vector2.INF or pts[i + 1] == Vector2.INF:
|
||
continue
|
||
var d := _dist_point_segment(screen_pos, pts[i], pts[i + 1])
|
||
if d < best_d:
|
||
best_d = d
|
||
best = a
|
||
return best
|
||
|
||
|
||
func _ring_screen_points(a: String) -> Array:
|
||
var axis := _axis_vec(a)
|
||
var u := axis.cross(Vector3.UP if absf(axis.dot(Vector3.UP)) < 0.9 else Vector3.RIGHT).normalized()
|
||
var v := axis.cross(u).normalized()
|
||
var out: Array = []
|
||
for i in RING_SEGMENTS + 1:
|
||
var t := TAU * float(i) / float(RING_SEGMENTS)
|
||
out.append(_safe_unproject(global_position + (u * cos(t) + v * sin(t)) * RING_R))
|
||
return out
|
||
|
||
|
||
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 _rot_dragging:
|
||
_finish_rotate(false)
|
||
return true
|
||
if not _dragging:
|
||
return false
|
||
_finish_drag(false)
|
||
return true
|
||
|
||
|
||
# --------------------------------------------------- Drehen / Kippen -------
|
||
|
||
func _ring_angle(mouse: Vector2) -> float:
|
||
var d := mouse - _rot_center
|
||
return atan2(d.y, d.x)
|
||
|
||
|
||
func _begin_rotate(axis: String, mouse: Vector2) -> void:
|
||
var inter = main.get("interaction")
|
||
if inter == null or inter.selected_indices.is_empty():
|
||
return
|
||
var c := _safe_unproject(global_position)
|
||
if c == Vector2.INF:
|
||
return
|
||
_rot_axis = axis
|
||
_rot_center = c
|
||
_rot_start_angle = _ring_angle(mouse)
|
||
_rot_applied = 0.0
|
||
_rot_serial += 1
|
||
_rot_suffix = "#g%d" % _rot_serial
|
||
_rot_dragging = true
|
||
var name := "Drehen (Y)" if axis == "y" else ("Kippen um X" if axis == "x" else "Kippen um Z")
|
||
main.call("_set_status", "%s — Ring ziehen, rastet in %.0f°-Schritten; Esc bricht ab, Strg+Z nimmt den Zug zurück." % [name, rot_step_deg])
|
||
|
||
|
||
func _rotate_motion(mouse: Vector2) -> void:
|
||
# Bildschirmwinkel → Drehwinkel um die Achse (Vorzeichen je nach Blickrichtung).
|
||
var delta_screen := wrapf(_ring_angle(mouse) - _rot_start_angle, -PI, PI)
|
||
var facing := _axis_vec(_rot_axis).dot(camera.global_transform.basis.z)
|
||
var deg := -rad_to_deg(delta_screen) * (1.0 if facing >= 0.0 else -1.0)
|
||
var snapped_deg := snappedf(deg, rot_step_deg)
|
||
_rotate_to(snapped_deg)
|
||
|
||
|
||
## Auf einen Gesamtwinkel (gerastet) nachführen — in Schritten, EIN Undo-Eintrag.
|
||
func _rotate_to(total_deg: float) -> void:
|
||
var diff := total_deg - _rot_applied
|
||
if absf(diff) < 0.001:
|
||
return
|
||
var inter = main.get("interaction")
|
||
if inter == null:
|
||
return
|
||
if _rot_axis == "y":
|
||
inter.rotate_selected(diff, _rot_suffix)
|
||
else:
|
||
inter.tilt_selected(_rot_axis == "x", diff, _rot_suffix)
|
||
_rot_applied = total_deg
|
||
|
||
|
||
func _finish_rotate(commit: bool) -> void:
|
||
if not _rot_dragging:
|
||
return
|
||
_rot_dragging = false
|
||
if not commit and absf(_rot_applied) > 0.001:
|
||
main.call("_do_undo") # der ganze Zug ist EIN Eintrag → komplett zurück
|
||
main.call("_set_status", "Dreh-Zug abgebrochen — Objekt wie vorher.")
|
||
_rot_axis = ""
|
||
_rot_applied = 0.0
|
||
|
||
|
||
## Probes/Tests: derselbe Pfad wie ein echter Ring-Zug (ohne Maus-Mathe).
|
||
func demo_rotate_axis(axis: String, deg: float) -> bool:
|
||
var inter = main.get("interaction") if main != null else null
|
||
if inter == null or inter.selected_indices.is_empty() or absf(deg) < 0.001:
|
||
return false
|
||
_rot_axis = axis
|
||
_rot_serial += 1
|
||
_rot_suffix = "#g%d" % _rot_serial
|
||
_rot_applied = 0.0
|
||
_rotate_to(snappedf(deg, rot_step_deg))
|
||
_rot_axis = ""
|
||
_rot_applied = 0.0
|
||
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
|