extends VBoxContainer ## Objekt-Inspektor (Karte t_bb0d11c9) — Godot/Unity-Style Eigenschaften-Panel ## oben im rechten Panel: Position/Drehung/Größe der markierten Objekte ## TIPPBAR (LineEdit) und ZIEHBAR (am Label ziehen, Godot-Konvention). ## ## Verdrahtung (main.gd): der Inspektor wird in _build_right_panel() als ## ERSTER Abschnitt eingehängt; refresh() läuft in _selection_status und ## _sync_selection_panel — damit füllen ALLE Auswahl-Wege das Panel ## (3D-Klick, Rahmen-Ausziehen, Map-Inhalt-Baum). ## ## Regeln der Karte: ## - Änderungen wirken LIVE im 3D-Fenster (Marker direkt umgesetzt, kein Rebuild). ## - JEDE Änderung ist EIN Strg+Z-Schritt: Tippen = ein Schritt beim Bestätigen ## (Enter/Klick woanders), Ziehen = ein Schritt beim Loslassen. ## - Mehrfachauswahl: Änderungen wirken auf ALLE markierten; ungleiche Werte ## zeigen „—“ und werden erst beim Tippen/Ziehen angeglichen. ## - SCALE IST FEST (Karte t_e166e656, Sly-Wunsch „Realismus“): die scale-Zeile ## ist bewusst ENTFERNT — neue Objekte bekommen immer scale 1.0, alte Karten ## laden mit ihrem gespeicherten scale (Format bleibt kompatibel). Hier gibt ## es nur den lesenden Hinweis „Größe fix“. ## - KIPPEN IN SÄMTLICHEN ACHSEN (Karte t_e166e656): das svmap-Feld `rot` ## (Euler x/y/z, OPTIONAL) trägt X/Z-Kippen; fehlt es, gilt rot_y_deg. ## rot/rot_y_deg bleiben über SvmapIO.rot_setzen synchron. ## - X/Z werden an der Map geklemmt, Y an den Match-Grenzen — Versenken UNTER ## die Terrain-Höhe ist ausdrücklich erlaubt (Steine im Boden), die ## Info-Zeile zeigt die Versenk-Tiefe. const EPS := 0.005 # Toleranz „gleicher Wert“ (Anzeige-Konsens) const MIXED := "—" # Anzeige für ungleiche Werte in der Auswahl func _clamp_norm_deg(f: float) -> float: return fposmod(f + 180.0, 360.0) - 180.0 class DragValue extends HBoxContainer: ## Eine Zeile „Label + Wert“: am Label ziehen ändert den Wert live ## (Godot-Inspector-Konvention), ins LineEdit tippen setzt ihn exakt. ## Die Logik (Wert lesen/anwenden/Undo) liegt beim Inspektor — dieses ## Widget ist nur die Eingabe-Maschine und ruft die Callables. var step := 0.05 # Wert-Änderung pro Pixel beim Ziehen var fmt := "%.2f" # Anzeige-Format var suffix := "" var min_v := -1000000.0 # Zieh-Grenzen (Anzeige + Doc werden geklemmt) var max_v := 1000000.0 var get_value: Callable # -> float (Wert des ersten markierten Objekts) var is_mixed: Callable # -> bool (true = Werte in der Auswahl ungleich) var begin_live: Callable # Undo-Basis merken (Zieh-Start) var live_apply: Callable # (float) — live anwenden OHNE Undo-Commit var end_live: Callable # (final: float, original: float) — Commit beim Loslassen var commit: Callable # (float) — Tippen: Snapshot + anwenden + EIN Commit var label: Label var edit: LineEdit var _dragging := false var _drag_base := 0.0 var _drag_accum := 0.0 func _init() -> void: add_theme_constant_override("separation", 6) func build(p_label: String, tip: String, edit_width: int) -> void: label = Label.new() label.text = p_label # KEINE feste Mindestbreite — die kompakte X/Y/Z-Zeile (3 Widgetpaare # nebeneinander) darf das Panel NICHT breiter drücken als das Fenster # (Layout-Bruch bei UI-Größe Groß, Beweis-Probe 19.10.). Natürliche # Breite von „X“/„Grad“/„Faktor“ reicht als Zieh-Fläche. label.mouse_filter = Control.MOUSE_FILTER_STOP label.tooltip_text = tip label.gui_input.connect(_on_label_input) add_child(label) edit = LineEdit.new() edit.custom_minimum_size = Vector2(edit_width, 0) edit.select_all_on_focus = true edit.text_submitted.connect(_on_text_submitted) edit.focus_exited.connect(_on_focus_exited) add_child(edit) func set_shown(v: float) -> void: edit.text = (fmt % v) + suffix func set_mixed() -> void: edit.text = "—" ## Probes/Tests: Ziehen programmgesteuert ausführen (gleicher Pfad wie Maus). func probe_drag(pixels: float) -> void: _start_drag() _drag_accum += pixels _apply_drag() _finish_drag() func _start_drag() -> void: _dragging = true _drag_accum = 0.0 _drag_base = float(get_value.call()) begin_live.call() func _apply_drag() -> void: var v := _clamp(_drag_base + _drag_accum * step) live_apply.call(v) set_shown(v) func _finish_drag() -> void: if not _dragging: return _dragging = false end_live.call(_clamp(_drag_base + _drag_accum * step), _drag_base) func _clamp(f: float) -> float: return clampf(f, min_v, max_v) func _on_label_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: _start_drag() else: _finish_drag() elif event is InputEventMouseMotion and _dragging: _drag_accum += event.relative.x _apply_drag() func _parse(text: String) -> float: var t := text.strip_edges().replace(",", ".") if t.is_empty() or not t.is_valid_float(): return NAN return t.to_float() func _apply_text(text: String) -> void: var v := _parse(text) if is_nan(v): set_shown(float(get_value.call())) # „—“/Müll → Anzeige wiederherstellen return commit.call(_clamp(v)) func _on_text_submitted(text: String) -> void: _apply_text(text) func _on_focus_exited() -> void: # Nur nachziehen, wenn der Text NICHT frisch bestätigt wurde (sonst # würde Enter + Klick woanders DOPPELT committen). if edit.text.is_empty(): return if edit.text.begins_with("—"): return # Bestätigte Werte wurden beim submitted bereits gesetzt und neu # formatiert; weicht der Text noch ab (Klick weg ohne Enter), gilt das # als bestätigt — EIN Undo-Schritt. _apply_text(edit.text) var _main # core/ui/main.gd (ungewollt typisiert — dynamisch) var _head: Label var _sub: Label var _grid: GridContainer var _info: Label var _hint: Label var _ground_btn: Button var _farben_grid: GridContainer # Farbfeld-Raster (Karte t_b0edb807) var _farbe_reset_btn: Button var _farbe_status: Label var _rows := {} # field -> DragValue var _built := false func setup(p_main) -> void: _main = p_main add_theme_constant_override("separation", 6) _head = Label.new() _head.text = "Objekt-Inspektor" _head.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART add_child(_head) _sub = Label.new() _sub.text = "" _sub.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART add_child(_sub) _hint = Label.new() _hint.text = "Nichts ausgewählt.\nKlicke im Viewport ein Objekt an." _hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART add_child(_hint) _grid = GridContainer.new() _grid.columns = 1 _grid.add_theme_constant_override("h_separation", 6) _grid.add_theme_constant_override("v_separation", 6) add_child(_grid) # --- Position X/Y/Z --- _grid.add_child(_field("Position")) var px := _make_row("pos_x", "X", "Position X in Meter — ZIEHEN am Label ändert live, TIPPEN setzt exakt (Enter bestätigt).", 0.05, "%.2f", " m") var py := _make_row("pos_y", "Y", "Höhe (Fußpunkt) in Meter — ziehen/tippen. UNTER die Terrain-Höhe absenken ist erlaubt (Versenken, Karte t_e166e656), Untergrenze = Match-Grenze.", 0.05, "%.2f", " m") var pz := _make_row("pos_z", "Z", "Position Z in Meter — ziehen/tippen.", 0.05, "%.2f", " m") # Zieh-Grenzen grob an der Map orientieren (doc-Apply klemmt exakt). var half_span: float = 1024.0 px.min_v = -half_span px.max_v = half_span py.min_v = -200.0 # Versenken erlaubt (Karte t_e166e656) — _apply klemmt exakt an bounds.min.y py.max_v = 80.0 pz.min_v = -half_span pz.max_v = half_span var pos_row := HBoxContainer.new() pos_row.add_theme_constant_override("separation", 6) pos_row.add_child(px) pos_row.add_child(py) pos_row.add_child(pz) _grid.add_child(pos_row) # --- Drehen & Kippen (Karte t_e166e656) --- _grid.add_child(_field("Drehen & Kippen")) _grid.add_child(_make_row("rot_y", "Drehen Y", "Blickrichtung in Grad (−180…180) — ziehen/tippen. Tasten: R (90°), Mausrad (15° fein).", 0.5, "%.1f", "°", -180.0, 180.0)) _grid.add_child(_make_row("rot_x", "Kippen X", "Kippen um die X-Achse in Grad (−180…180) — vor/zurück neigen. Tasten: Strg+R / Strg+Umschalt+R.", 0.5, "%.1f", "°", -180.0, 180.0)) _grid.add_child(_make_row("rot_z", "Kippen Z", "Kippen um die Z-Achse in Grad (−180…180) — links/rechts neigen. Tasten: Alt+R / Alt+Umschalt+R.", 0.5, "%.1f", "°", -180.0, 180.0)) # --- Größe (FEST, Karte t_e166e656) --- _grid.add_child(_field("Größe")) var gross_fix := Label.new() gross_fix.text = "Größe fix (Original) — Assets bleiben in ihrer echten Größe." gross_fix.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _grid.add_child(gross_fix) var btn_row := HBoxContainer.new() btn_row.add_theme_constant_override("separation", 6) _ground_btn = Button.new() _ground_btn.text = "Auf Boden setzen" _ground_btn.custom_minimum_size = UIScale.v2(0, 40) _ground_btn.tooltip_text = "Alle markierten Objekte exakt auf die Terrain-Höhe setzen (Strg+Bild↑ geht auch) — EIN Rückgängig-Schritt" _ground_btn.pressed.connect(_on_ground) btn_row.add_child(_ground_btn) _grid.add_child(btn_row) # --- Farbe / Material (Karte t_b0edb807) --- # Feste Palette als Farbfeld-Raster; Klick = Farbe auf ALLE markierten # Objekte (EIN Undo-Schritt). „Zurücksetzen“ entfernt das tint-Feld → # wieder die Pack-Farbe (Rückwärtskompatibel, wie vor der Karte). _grid.add_child(_field("Farbe / Material")) _farben_grid = GridContainer.new() _farben_grid.columns = 4 _farben_grid.add_theme_constant_override("h_separation", 6) _farben_grid.add_theme_constant_override("v_separation", 6) _grid.add_child(_farben_grid) for eintrag in SvmapTint.PALETTE: _farben_grid.add_child(_swatch(eintrag[1], str(eintrag[0]))) var farbe_row := HBoxContainer.new() farbe_row.add_theme_constant_override("separation", 6) _farbe_reset_btn = Button.new() _farbe_reset_btn.text = "Zurücksetzen (Pack-Farbe)" _farbe_reset_btn.custom_minimum_size = UIScale.v2(0, 40) _farbe_reset_btn.tooltip_text = "Farbe der markierten Objekte entfernen — sie zeigen wieder die Pack-Farbe — EIN Rückgängig-Schritt" _farbe_reset_btn.pressed.connect(_on_farbe_reset) farbe_row.add_child(_farbe_reset_btn) _grid.add_child(farbe_row) _farbe_status = Label.new() _farbe_status.text = "" _grid.add_child(_farbe_status) # --- Info (nur lesen) --- _info = Label.new() _info.text = "" _info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART add_child(_info) # Startzustand: keine Auswahl → nur Hinweis sichtbar (refresh() schaltet um). _grid.visible = false _info.visible = false _ground_btn.visible = false _built = true func _field(text: String) -> Label: var l := Label.new() l.text = text l.tooltip_text = "Werte gelten für ALLE markierten Objekte. „—“ = Werte sind verschieden und werden erst beim Tippen/Ziehen angeglichen." return l ## Ein Farbfeld-Knopf (Karte t_b0edb807): flacher Farb-Button mit Tooltip. func _swatch(c: Color, name: String) -> Button: var btn := Button.new() btn.custom_minimum_size = UIScale.v2(34, 34) btn.tooltip_text = "%s — Farbe auf alle markierten Objekte anwenden (EIN Rückgängig-Schritt)" % name btn.focus_mode = Control.FOCUS_NONE var sb := StyleBoxFlat.new() sb.bg_color = c sb.set_corner_radius_all(6) btn.add_theme_stylebox_override("normal", sb) var sb_hover: StyleBoxFlat = sb.duplicate() sb_hover.bg_color = c.lightened(0.08) # U7: Aufhellung statt Manuell-Mathe btn.add_theme_stylebox_override("hover", sb_hover) btn.add_theme_stylebox_override("pressed", sb_hover) btn.add_theme_stylebox_override("focus", StyleBoxEmpty.new()) btn.pressed.connect(_on_farbe_gewaehlt.bind(c, name)) return btn # ------------------------------------------------------------- Farbe/Material --- ## Palette-Farbe auf ALLE markierten Objekte — EIN Undo-Schritt. func _on_farbe_gewaehlt(c: Color, name: String) -> void: var indices := _selected() if indices.is_empty(): return var before: Dictionary = _main.history.snapshot(["objects"]) var objects: Array = _main.doc.get("objects", []) var changed := false for i in indices: var obj: Dictionary = objects[i] var alt := SvmapTint.von(obj) SvmapTint.setze(obj, c) if not SvmapTint.gleich(alt, c): changed = true if not changed: return # gleiche Farbe erneut → kein Leer-Schritt _commit_common("Inspektor: Farbe %s" % name, before) _main.view_builder.call("tint_refresh", indices) ## tint-Feld entfernen → Pack-Farbe — EIN Undo-Schritt. func _on_farbe_reset() -> void: var indices := _selected() if indices.is_empty(): return var before: Dictionary = _main.history.snapshot(["objects"]) var objects: Array = _main.doc.get("objects", []) var changed := false for i in indices: var obj: Dictionary = objects[i] if SvmapTint.hat(obj): SvmapTint.entferne(obj) changed = true if not changed: return # nichts hatte eine Farbe → kein Leer-Schritt _commit_common("Inspektor: Farbe zurückgesetzt (Pack-Farbe)", before) _main.view_builder.call("tint_refresh", indices) ## Status-Zeile der Farbsektion: Pack-Farbe / Palettenname / eigene Farbe / ## gemischt (Mehrfachauswahl mit verschiedenen Farben). func _farbe_text(objs: Array) -> String: if objs.is_empty(): return "" if objs.size() == 1: var obj: Dictionary = objs[0] if not SvmapTint.hat(obj): return "Farbe: Pack-Farbe" var c := SvmapTint.von(obj) var pn := SvmapTint.palette_name(c) return "Farbe: %s" % (pn if pn != "" else "eigene Farbe") # Mehrfachauswahl: alle ohne Feld = Pack-Farbe, alle gleich = Name, sonst gemischt. var first_hat := SvmapTint.hat(objs[0]) var first_c := SvmapTint.von(objs[0]) for obj in objs: if SvmapTint.hat(obj) != first_hat: return "Farbe: gemischt" if first_hat and not SvmapTint.gleich(SvmapTint.von(obj), first_c): return "Farbe: gemischt" if not first_hat: return "Farbe: Pack-Farbe" var pn2 := SvmapTint.palette_name(first_c) return "Farbe: %s" % (pn2 if pn2 != "" else "eigene Farbe") func _make_row(field: String, lbl: String, tip: String, step: float, fmt: String, suffix: String, min_v := -1000000.0, max_v := 1000000.0) -> DragValue: var w := DragValue.new() w.step = step w.fmt = fmt w.suffix = suffix w.min_v = min_v w.max_v = max_v w.get_value = _get_value.bind(field) w.is_mixed = _is_mixed.bind(field) w.begin_live = _begin_live w.live_apply = _live_apply.bind(field) w.end_live = _end_live.bind(field) w.commit = _commit.bind(field) w.build(lbl, tip, UIScale.px(64)) _rows[field] = w return w # ------------------------------------------------------------- Datenzugriff -- func _selected() -> Array: if _main == null or _main.get("interaction") == null: return [] var objects: Array = _main.doc.get("objects", []) var out: Array = [] for i in _main.interaction.selected_indices: if i is int and i >= 0 and i < objects.size(): out.append(i) return out func _sel_objects() -> Array: var objects: Array = _main.doc.get("objects", []) var out: Array = [] for i in _selected(): var obj = objects[i] if obj is Dictionary: out.append(obj) return out func _entry(obj: Dictionary) -> Dictionary: var manifest: Dictionary = _main.get("manifest") if manifest == null or manifest.is_empty(): return {} var lib: Dictionary = manifest.get("objects", {}) return lib.get(str(obj.get("id", "")), {}) func _field_value(obj: Dictionary, field: String) -> float: match field: "pos_x": var p: Array = obj.get("pos", [0.0, 0.0, 0.0]) return float(p[0]) if (p as Array).size() == 3 else 0.0 "pos_y": var p: Array = obj.get("pos", [0.0, 0.0, 0.0]) return float(p[1]) if (p as Array).size() == 3 else 0.0 "pos_z": var p: Array = obj.get("pos", [0.0, 0.0, 0.0]) return float(p[2]) if (p as Array).size() == 3 else 0.0 "rot_y": return _clamp_norm_deg(SvmapIO.objekt_rotation(obj).y) "rot_x": return _clamp_norm_deg(SvmapIO.objekt_rotation(obj).x) "rot_z": return _clamp_norm_deg(SvmapIO.objekt_rotation(obj).z) return 0.0 func _get_value(field: String) -> float: var objs := _sel_objects() if objs.is_empty(): return 0.0 return _field_value(objs[0], field) func _is_mixed(field: String) -> bool: var objs := _sel_objects() if objs.size() < 2: return false var first := _field_value(objs[0], field) for obj in objs: if absf(_field_value(obj, field) - first) > EPS: return true return false # ------------------------------------------------------------ Anzeige (refresh) -- func refresh() -> void: if not _built or _main == null or _main.doc.is_empty(): return var indices := _selected() var objects: Array = _main.doc.get("objects", []) var has := not indices.is_empty() _grid.visible = has _ground_btn.visible = has _info.visible = has _hint.visible = not has if not has: _head.text = "Objekt-Inspektor" _sub.text = "" return var objs := _sel_objects() var m := objs.size() # Kopf: Name(n) + ID + Kategorie + „N von M markiert“ + Poly. var names := PackedStringArray() for obj in objs: if names.size() < 3: names.append(str(LibraryBrowser.object_label(str(obj.get("id", "?"))))) var more := " … +%d" % (m - 3) if m > 3 else "" var name_txt := (" · ".join(names) + more) if m > 1 else str(LibraryBrowser.object_label(str(objs[0].get("id", "?")))) _head.text = name_txt var kat := "?" var oid := str((objs[0] as Dictionary).get("id", "")) var entry: Dictionary = _entry(objs[0]) if not entry.is_empty(): kat = str(entry.get("kategorie", "?")) var poly := 0.0 for obj in objs: var e: Dictionary = _entry(obj) poly += float(e.get("poly", 0)) * float(obj.get("scale", 1.0)) * float(obj.get("scale", 1.0)) _sub.text = "ID: %s · Kategorie: %s · %d von %d Objekten markiert · Poly: %d" % [ oid if m == 1 else "%s …" % oid, kat, m, objects.size(), int(poly)] for field in ["pos_x", "pos_y", "pos_z", "rot_y", "rot_x", "rot_z"]: var w: DragValue = _rows[field] if w.edit.has_focus(): continue # nicht unter dem tippenden Nutzer wegziehen if _is_mixed(field): w.set_mixed() else: w.set_shown(_get_value(field)) # Farbsektion (Karte t_b0edb807): aktueller Farbstand der Auswahl. _farbe_status.text = _farbe_text(objs) # Info-Block (nur lesen). var lift := ObjectOps.lift_offset(_main.doc, indices[0]) var lift_txt: String if absf(lift) < ObjectOps.FLOAT_EPSILON: lift_txt = "am Boden" elif lift > 0.0: lift_txt = "%+.2f m über dem Boden" % lift else: lift_txt = "%.2f m im Boden versenkt" % -lift var e0: Dictionary = _entry(objs[0]) var col: Dictionary = e0.get("collision", {}) var col_size: Array = col.get("size", []) var col_txt := "—" if (col_size as Array).size() == 3: var s := float(objs[0].get("scale", 1.0)) col_txt = "%s %s × %s × %s m" % [ ("Box" if str(col.get("shape", "box")) == "box" else str(col.get("shape", "?"))), ("%.1f" % (float(col_size[0]) * s)), ("%.1f" % (float(col_size[1]) * s)), ("%.1f" % (float(col_size[2]) * s))] var echt := _echt_modell(indices[0]) _info.text = "Fußpunkt: y = %.2f m (%s)\nKollision: %s\nEchtes Modell: %s" % [ float((objs[0] as Dictionary).get("pos", [0.0, 0.0, 0.0])[1]) if ((objs[0] as Dictionary).get("pos", [0.0, 0.0, 0.0]) as Array).size() == 3 else 0.0, lift_txt, col_txt, "ja" if echt else "nein (Platzhalter)"] func _echt_modell(idx: int) -> bool: var markers: Array = _main.view_builder.object_markers if idx < markers.size(): var marker = markers[idx] if marker != null and is_instance_valid(marker) and marker.has_meta("glb"): return bool(marker.get_meta("glb")) return not str(_entry(_main.doc["objects"][idx]).get("glb", "")).is_empty() # ------------------------------------------------------------ Anwenden + Undo -- var _live_before: Dictionary = {} # Undo-Basis während des Ziehens var _live_changed := false # hat das Ziehen das Doc wirklich geändert? func _begin_live() -> void: _live_before = _main.history.snapshot(["objects"]) _live_changed = false ## Achtung Callable.bind hängt die gebundenen Argumente HINTER die ## Aufruf-Argumente: commit.call(v) → _commit(v, field). func _live_apply(v: float, field: String) -> void: if _apply(field, v): _live_changed = true func _end_live(final: float, original: float, field: String) -> void: if not _live_changed or absf(final - original) < 0.0001: return # Ziehen ohne Änderung → kein Leer-Undo-Schritt _commit_common("Inspektor: %s" % _desc_for(field, final), _live_before) func _commit(v: float, field: String) -> void: var before: Dictionary = _main.history.snapshot(["objects"]) if not _apply(field, v): return # nichts geändert (alles geklemmt/identisch) → kein Leer-Schritt _commit_common("Inspektor: %s" % _desc_for(field, v), before) func _commit_common(desc: String, before: Dictionary) -> void: _main.history.commit(desc, before, _main.history.snapshot(["objects"])) _main.dirty = true _main.call("_refresh_file_label") _main.call("_refresh_stats") _main.call("_refresh_history_buttons") _main.call("_refresh_spawn_status") _main.call("_set_status", "%s — Strg+Z macht es in EINEM Schritt rückgängig." % desc) refresh() func _desc_for(field: String, v: float) -> String: var n := _sel_objects().size() var mehr := "%d Objekte: " % n if n > 1 else "" match field: "pos_x": return "%sPosition X = %.2f m" % [mehr, v] "pos_y": return "%sHöhe (Y) = %.2f m" % [mehr, v] "pos_z": return "%sPosition Z = %.2f m" % [mehr, v] "rot_y": return "%sDrehen (Y) = %.1f°" % [mehr, v] "rot_x": return "%sKippen (X) = %.1f°" % [mehr, v] "rot_z": return "%sKippen (Z) = %.1f°" % [mehr, v] return "Inspektor-Änderung" ## Wert auf ALLE markierten Objekte anwenden + 3D-Ansicht live aktualisieren. ## false = nichts geändert (keine Auswahl / alles geklemmt). func _apply(field: String, v: float) -> bool: var indices := _selected() if indices.is_empty(): return false var objects: Array = _main.doc.get("objects", []) var half: Vector2 = _terrain_half() var max_y := _max_y() var min_y := _min_y() var changed := 0 # Position-Felder bei Mehrfachauswahl: der eingetippte Wert gilt für das # ERSTE markierte Objekt (Anker), alle anderen wandern mit DEMSELBEN Delta # mit → Abstände bleiben erhalten (Relativ-Move, wie beim Greifen). # Bei 1 Objekt ist das Delta = absoluter Wert → Verhalten unverändert. var rel_delta := 0.0 if field == "pos_x" or field == "pos_y" or field == "pos_z": var anchor_pos: Array = (objects[indices[0]] as Dictionary).get("pos", [0.0, 0.0, 0.0]) if (anchor_pos as Array).size() == 3: var comp := 0 if field == "pos_x" else (1 if field == "pos_y" else 2) rel_delta = v - float(anchor_pos[comp]) for i in indices: 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 := float(pos[0]) var y := float(pos[1]) var z := float(pos[2]) if field == "rot_y" or field == "rot_x" or field == "rot_z": # Karte t_e166e656: rot/rot_y_deg bleiben über rot_setzen synchron # (reines Y → kein rot-Feld in der Datei). Anzeige normalisiert # auf −180…180, gespeichert wird der Wert wie getippt. var euler := SvmapIO.objekt_rotation(obj) var neu := Vector3(euler.x, euler.y, euler.z) if field == "rot_y": neu.y = _clamp_norm_deg(v) elif field == "rot_x": neu.x = _clamp_norm_deg(v) else: neu.z = _clamp_norm_deg(v) SvmapIO.rot_setzen(obj, neu) changed += 1 continue if field == "pos_x": x = clampf(float(pos[0]) + rel_delta, -half.x, half.x) elif field == "pos_y": # Karte t_e166e656: Versenken erlaubt — Untergrenze ist die # Match-Grenze (bounds.min.y), NICHT mehr die Terrain-Höhe. y = clampf(float(pos[1]) + rel_delta, min_y, max_y) elif field == "pos_z": z = clampf(float(pos[2]) + rel_delta, -half.y, half.y) if absf(x - float(pos[0])) > 0.0001 or absf(y - float(pos[1])) > 0.0001 or absf(z - float(pos[2])) > 0.0001: obj["pos"] = [x, y, z] changed += 1 _sync_markers(indices) return changed > 0 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", {}) if (bounds.get("max", null) is Array) and ((bounds["max"] as Array).size() == 3): return float(bounds["max"][1]) return 80.0 ## Untere Match-Grenze (Karte t_e166e656): Versenken im Boden ist erlaubt, ## klemmt an bounds.min.y statt an der Terrain-Höhe. func _min_y() -> float: var bounds: Dictionary = _main.doc.get("bounds", {}) if (bounds.get("min", null) is Array) and ((bounds["min"] as Array).size() == 3): return float(bounds["min"][1]) return -80.0 func _sync_markers(indices: Array) -> void: var markers: Array = _main.view_builder.object_markers var objects: Array = _main.doc.get("objects", []) for i in indices: if i >= markers.size(): continue var marker = markers[i] if marker == null or not is_instance_valid(marker): continue var pos: Array = (objects[i] as Dictionary).get("pos", [0.0, 0.0, 0.0]) if (pos as Array).size() == 3: marker.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2])) marker.rotation_degrees = SvmapIO.objekt_rotation(objects[i] as Dictionary) # ------------------------------------------------------------------- Knöpfe -- func _on_ground() -> void: # Bewährte Boden-Logik (interaction.snap_selection_to_floor): snapshotet, # committet selbst EINEN Schritt und meldet in der Statuszeile. _main.interaction.call("snap_selection_to_floor") refresh() ## Für Tests/Probes: Widget zu einem Feld holen ("pos_x"…"rot_z"). func widget(field: String) -> DragValue: return _rows.get(field)