diff --git a/src/ui/inspector_panel.gd b/src/ui/inspector_panel.gd index 821eaec..87ff0f0 100644 --- a/src/ui/inspector_panel.gd +++ b/src/ui/inspector_panel.gd @@ -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 diff --git a/tests/probe_multi_move.gd b/tests/probe_multi_move.gd new file mode 100644 index 0000000..1beba25 --- /dev/null +++ b/tests/probe_multi_move.gd @@ -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) diff --git a/tests/probe_multi_move.gd.uid b/tests/probe_multi_move.gd.uid new file mode 100644 index 0000000..d07ede4 --- /dev/null +++ b/tests/probe_multi_move.gd.uid @@ -0,0 +1 @@ +uid://r4phip53o7fv diff --git a/tests/test_inspector.gd b/tests/test_inspector.gd index bd7b332..cd8d08e 100644 --- a/tests/test_inspector.gd +++ b/tests/test_inspector.gd @@ -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: