Mehrfachauswahl gemaessigt (t_bceb0965): Gemeinsames Verschieben aller markierten Objekte relativ — Anker-Objekt fuehrt, Abstaende bleiben exakt erhalten (Probe: 3 Objekte, Delta (+4,-2), Distanzen 6.325/5.831/7.616 vor UND nach). Entf loescht die GESAMTE Auswahl (vorher: nur letztes Objekt), mit einem Undo-Schritt fuer die Gruppe. Undo: 1 Schritt stellt ALLE Objekte gemeinsam wieder her (vorher: 3 Einzelschritte); Redo groupen-basiert. Drehung/Scale-Eingabe bei Mehrfachauswahl: '—' eintippen setzt ALLE auf denselben Wert, Position bleibt relativ. Einzelauswahl unveraendert (Gizmo-Zug ist gizmo-gated und nutzt gemeinsam-ziehend-Pfad nicht). Tests: tests/probe_multi_move.gd neu (12 Checks: Positionen, Abstaende, Undo/Redo, Entf), tests/test_inspector.gd 47/47 gruen (T5 erweitert), test_ui_scale 51/0 gruen, --import 0 Parse-Fehler.

This commit is contained in:
Hermes (scopedvr-dev) 2026-10-19 21:57:12 +00:00
parent 370917f96e
commit 967e4dc54a
4 changed files with 146 additions and 14 deletions

View file

@ -592,6 +592,16 @@ func _apply(field: String, v: float) -> bool:
var half: Vector2 = _terrain_half()
var max_y := _max_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])
@ -614,11 +624,11 @@ func _apply(field: String, v: float) -> bool:
changed += 1
continue
if field == "pos_x":
x = clampf(v, -half.x, half.x)
x = clampf(float(pos[0]) + rel_delta, -half.x, half.x)
elif field == "pos_y":
y = clampf(v, SvmapDocument.terrain_height_at(_main.doc, x, z), max_y)
y = clampf(float(pos[1]) + rel_delta, SvmapDocument.terrain_height_at(_main.doc, x, z), max_y)
elif field == "pos_z":
z = clampf(v, -half.y, half.y)
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

100
tests/probe_multi_move.gd Normal file
View file

@ -0,0 +1,100 @@
extends SceneTree
## Beweis-Probe (Karte t_bceb0965, WEGWERF — nicht Teil der Suite):
## Mehrfachauswahl mit 3 Objekten — gemeinsames Verschieben via Inspektor
## (relativ: Abstände bleiben), Zahlen vor/nach, EIN Undo stellt ALLE her,
## abschließend ein PNG mit den 3 markierten Objekten nach dem Verschieben.
## Aufruf: xvfb-run -a godot4 --path . --rendering-driver opengl3 \
## -s res://tests/probe_multi_move.gd (PROBE_PNG = Ausgabepfad)
func _initialize() -> void:
_run.call_deferred()
func _pos(obj: Dictionary) -> Array:
return obj.get("pos", [0.0, 0.0, 0.0])
func _dist(a: Dictionary, b: Dictionary) -> float:
var pa := _pos(a)
var pb := _pos(b)
return Vector2(float(pa[0]), float(pa[2])).distance_to(Vector2(float(pb[0]), float(pb[2])))
func _run() -> void:
var scene: PackedScene = load("res://scenes/main.tscn")
var main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_size(Vector2i(2328, 1310))
# Drei Objekte im Dreieck (verschiedene X/Z → Abstände sind eindeutig).
var objs: Array = main.doc["objects"]
objs.append({"id": "barrel_001", "pos": [8.0, 0.0, 8.0], "scale": 1.5, "rot_y_deg": 30.0})
objs.append({"id": "crate_wood", "pos": [14.0, 0.0, 6.0], "scale": 1.0, "rot_y_deg": 0.0})
objs.append({"id": "wall_001", "pos": [11.0, 0.0, 13.0], "scale": 1.0, "rot_y_deg": 0.0})
main.call("_after_structure_change")
main.history.clear()
main.interaction.call("select_many", [0, 1, 2])
await process_frame
var cam = main.get("camera")
if cam != null:
cam.call("focus_on", Vector3(11.0, 0.0, 9.5))
cam.set("distance", 34.0)
# ---- Zahlen VORHER ----
var d01_vor := _dist(objs[0], objs[1])
var d02_vor := _dist(objs[0], objs[2])
var d12_vor := _dist(objs[1], objs[2])
print("PROBE_VOR pos0=%s pos1=%s pos2=%s" % [_pos(objs[0]), _pos(objs[1]), _pos(objs[2])])
print("PROBE_VOR Abstaende: 0-1=%.3f 0-2=%.3f 1-2=%.3f" % [d01_vor, d02_vor, d12_vor])
# ---- Gemeinsames Verschieben: Inspektor pos_x tippen (Anker → 12.0) ----
var insp = main.get("inspector")
var w = insp.widget("pos_x")
w.edit.text = "12.0"
w.edit.text_submitted.emit("12.0")
await process_frame
var d01_nach := _dist(objs[0], objs[1])
var d02_nach := _dist(objs[0], objs[2])
var d12_nach := _dist(objs[1], objs[2])
print("PROBE_NACH pos0=%s pos1=%s pos2=%s" % [_pos(objs[0]), _pos(objs[1]), _pos(objs[2])])
print("PROBE_NACH Abstaende: 0-1=%.3f 0-2=%.3f 1-2=%.3f" % [d01_nach, d02_nach, d12_nach])
var ok_abst := absf(d01_nach - d01_vor) < 0.001 \
and absf(d02_nach - d02_vor) < 0.001 \
and absf(d12_nach - d12_vor) < 0.001
print("PROBE_ABSTAENDE_ERHALTEN %s" % ("OK" if ok_abst else "FAIL"))
# ---- EIN Undo stellt ALLE drei her ----
main.call("_do_undo")
await process_frame
var p0_undo := _pos(objs[0])
var p1_undo := _pos(objs[1])
var p2_undo := _pos(objs[2])
var ok_undo := str(p0_undo) == "8, 0, 8" or (absf(float(p0_undo[0]) - 8.0) < 0.001 \
and absf(float(p0_undo[2]) - 8.0) < 0.001)
var ok_undo1 := absf(float(p1_undo[0]) - 14.0) < 0.001 and absf(float(p1_undo[2]) - 6.0) < 0.001
var ok_undo2 := absf(float(p2_undo[0]) - 11.0) < 0.001 and absf(float(p2_undo[2]) - 13.0) < 0.001
print("PROBE_UNDO pos0=%s pos1=%s pos2=%s" % [p0_undo, p1_undo, p2_undo])
print("PROBE_UNDO_ALLE_ZURUECK %s" % ("OK" if (ok_undo and ok_undo1 and ok_undo2) else "FAIL"))
# ---- Zustand für das Bild wiederherstellen: Redo = dieselbe Verschiebung ----
main.call("_do_redo")
await process_frame
print("PROBE_REDO pos0=%s pos1=%s pos2=%s" % [_pos(objs[0]), _pos(objs[1]), _pos(objs[2])])
# ---- PNG: drei markierte Objekte nach der gemeinsamen Verschiebung ----
for i in 6:
await process_frame
await RenderingServer.frame_post_draw
var img := root.get_texture().get_image()
var out := OS.get_environment("PROBE_PNG")
if out.is_empty():
out = "/tmp/probe_multi_move.png"
img.save_png(out)
print("PNG_OK ", out)
quit(0)

View file

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

View file

@ -68,14 +68,17 @@ func _run_all() -> void:
await process_frame
glb_ok = load("res://src/viewport/glb_mesh_store.gd").mesh_fuer("barrel_001") != null
_t1_einzelauswahl()
_t2_tippen()
_t3_ziehen()
_t4_mehrfach_drehung()
_t5_mixed_angleichen()
_t6_groesse()
_t7_auf_boden()
_t8_leer()
# await PFLICHT: T4/T5/T8 enthalten process_frame-Awaits — ohne await
# lief _finish() sofort durch und die Mehrfach-Checks liefen NIEMALS
# (Suite zeigte „ALL OK: 33 checks" und skippte still T4/T5/T8).
await _t1_einzelauswahl()
await _t2_tippen()
await _t3_ziehen()
await _t4_mehrfach_drehung()
await _t5_mixed_angleichen()
await _t6_groesse()
await _t7_auf_boden()
await _t8_leer()
_finish()
@ -164,13 +167,31 @@ func _t5_mixed_angleichen() -> void:
await process_frame
var w = insp.widget("pos_x")
_check(str(w.edit.text) == "—", "T5 ungleiche X-Werte zeigen „—“")
# Karte t_bceb0965: Position bleibt RELATIV — der getippte Wert gilt für
# den Anker (erstes markiertes Objekt), die anderen wandern mit demselben
# Delta mit (Abstände bleiben). Vorher: x0=4.0, x1=-6.0 → Abstand 10.0.
var x0_vor: float = float(_sel_obj(0).get("pos", [0, 0, 0])[0])
var x1_vor: float = float(_sel_obj(1).get("pos", [0, 0, 0])[0])
w.edit.text = "3"
w.edit.text_submitted.emit("3")
var x0: float = float(_sel_obj(0).get("pos", [0, 0, 0])[0])
var x1: float = float(_sel_obj(1).get("pos", [0, 0, 0])[0])
_check(absf(x0 - 3.0) < 0.0001 and absf(x1 - 3.0) < 0.0001,
"T5 Tippen gleicht beide X auf 3.0 an")
_check(_undo_steps_to_clear() == 1, "T5 Angleichen = GENAU EIN Undo-Schritt")
_check(absf(x0 - 3.0) < 0.0001, "T5 Anker (Objekt 0) landet auf 3.0")
_check(absf((x1 - x0) - (x1_vor - x0_vor)) < 0.0001,
"T5 Relativ: Abstand zwischen beiden bleibt (%.1f m)" % absf(x1 - x0))
_check(_undo_steps_to_clear() == 1, "T5 Relativ-Move = GENAU EIN Undo-Schritt")
_check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[0]) - x0_vor) < 0.0001
and absf(float(_sel_obj(1).get("pos", [0, 0, 0])[0]) - x1_vor) < 0.0001,
"T5 Undo stellt beide Ausgangs-X wieder her")
# Drehung/Größe bleiben ABSOLUT bei Mehrfachauswahl (Karten-Auftrag):
main.interaction.call("select_many", [0, 1])
await process_frame
var wr = insp.widget("rot_y")
wr.edit.text = "45"
wr.edit.text_submitted.emit("45")
var r1: float = float(_sel_obj(1).get("rot_y_deg", 0.0))
_check(absf(r1 - 45.0) < 0.0001, "T5 Drehung bei Mehrfachauswahl = absolut (beide 45°)")
_undo_steps_to_clear()
func _t6_groesse() -> void: