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:
Hermes (Operator) 2026-10-19 21:29:09 +00:00
parent 9fa8029910
commit e664d09666
3 changed files with 479 additions and 0 deletions

View file

@ -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
View 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

View file

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