extends SceneTree ## Editor-Test (headless): Objekt-Inspektor — Karte t_bb0d11c9. ## Ausführen: godot --headless --path . --script res://tests/test_inspector.gd ## Exit 0 = alles grün. ## ## Instanziert die ECHTE Main-UI (wie test_gui_smoke) und prüft: ## T1 Kopf/Sub/Info füllen bei Einzel-Auswahl (Name, ID, Kategorie, ## „N von M“, Poly, Kollision, echtes Modell ja/nein). ## T2 Position TIPPEN → doc geändert + 3D-Marker live; EIN Undo-Schritt. ## T3 Position ZIEHEN (Probe-Drag wie Maus) → doc geändert; EIN Undo-Schritt. ## T4 Mehrfachauswahl: Drehung tippen → beide Objekte gleich; EIN Undo-Schritt. ## T5 Uneinheitliche Werte → „—“; Tippen gleicht ALLE an. ## T6 Größe: Klemmung 0,05–20 + Marker-Rescale (echtes Modell) + Zurücksetzen. ## T7 „Auf Boden setzen“ nutzt die Boden-Logik; EIN Undo-Schritt. ## T8 Keine Auswahl → Felder aus, Hinweis an. var failures := 0 var checks := 0 var main var insp var glb_ok := false # GLB-Quelle verfügbar? (sonst wie test_glb_view umgebungsabhängig) func _initialize() -> void: _run_all.call_deferred() func _check(cond: bool, label: String) -> void: checks += 1 print("%s: %s" % ["ok " if cond else "FAIL", label]) if not cond: failures += 1 func _undo_steps_to_clear() -> int: ## Wie viele Undo-Schritte nötig, bis die History leer ist (Baseline clear). var n := 0 while main.history.can_undo() and n < 50: main.call("_do_undo") n += 1 return n func _run_all() -> void: var scene: PackedScene = load("res://scenes/main.tscn") _check(scene != null, "scenes/main.tscn laedt") if scene == null: _finish() return main = scene.instantiate() root.add_child(main) await process_frame await process_frame insp = main.get("inspector") _check(insp != null, "Inspektor ist in main eingebaut") if insp == null: _finish() return # Zwei Objekte ins Doc: barrel_001 (echtes GLB) + crate_wood (Platzhalter). (main.doc["objects"] as Array).append({ "id": "barrel_001", "pos": [4.0, 0.0, 4.0], "scale": 1.0, "rot_y_deg": 0.0}) (main.doc["objects"] as Array).append({ "id": "crate_wood", "pos": [-6.0, 0.0, 2.0], "scale": 2.0, "rot_y_deg": 10.0}) main.call("_after_structure_change") main.history.clear() 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() _finish() func _select(idx: int) -> void: # select_object ist synchron; das Panel füllt im selben Aufruf (refresh). main.interaction.call("select_object", idx) func _sel_obj(idx: int) -> Dictionary: return (main.doc["objects"] as Array)[idx] func _t1_einzelauswahl() -> void: _select(0) var head: String = str(insp.get("_head").text) var sub: String = str(insp.get("_sub").text) var info: String = str(insp.get("_info").text) _check(not head.is_empty() and head != "Objekt-Inspektor", "T1 Kopf zeigt Objekt-Namen") _check(sub.contains("ID: barrel_001"), "T1 Sub zeigt ID") _check(sub.contains("Kategorie: props"), "T1 Sub zeigt Kategorie") _check(sub.contains("1 von 2 Objekten markiert"), "T1 Sub zeigt 'N von M markiert'") _check(sub.contains("Poly: 184"), "T1 Sub zeigt Poly-Zahl") _check(info.contains("Kollision:"), "T1 Info zeigt Kollisionsform") # „Echtes Modell ja“ braucht die GLB-Quellen (Geschwister-Ordner) — fehlen # sie im Workspace-Klon, zeigt das Panel korrekt „nein“ (wie test_glb_view # umgebungsabhängig; der „nein“-Pfad wird über crate_wood geprüft). if glb_ok: _check(info.contains("Echtes Modell: ja"), "T1 echtes Modell ja (barrel_001 mit GLB)") else: print("SKIP: T1 echtes-Modell-ja (keine GLB-Quelle im Workspace — Platzhalter-„nein“ ist geprüft)") _check(str(insp.widget("pos_x").edit.text).contains("4.00"), "T1 pos_x zeigt 4.00") _check(str(insp.widget("rot_y").edit.text).contains("0.0"), "T1 rot_y zeigt 0.0") # Auswahl-Beweis per Klick im Viewport-Kontext: Panel ist sichtbar. _check(insp.get("_grid").visible, "T1 Felder sichtbar bei Auswahl") # Fix Vorbestand: das alte „Auswahl“-Feld füllt sich jetzt auch bei # select_object (Vorher stand dort dauerhaft „Nichts ausgewählt.“). _check(str(main.get("sel_label").text).contains("Barrel"), "T1 altes Auswahl-Feld füllt bei 3D-Klick-Weg") func _t2_tippen() -> void: _select(0) var w = insp.widget("pos_x") w.edit.text = "12.5" w.edit.text_submitted.emit("12.5") var px: float = float(_sel_obj(0).get("pos", [0, 0, 0])[0]) _check(absf(px - 12.5) < 0.0001, "T2 Tippen ändert doc-Position auf 12.5") var marker = main.view_builder.object_markers[0] _check(marker != null and absf(marker.position.x - 12.5) < 0.0001, "T2 3D-Marker folgt live (position.x = 12.5)") _check(_undo_steps_to_clear() == 1, "T2 Tippen = GENAU EIN Undo-Schritt") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[0]) - 4.0) < 0.0001, "T2 Undo stellt 4.0 wieder her") func _t3_ziehen() -> void: _select(0) var base: float = float(_sel_obj(0).get("pos", [0, 0, 0])[2]) var w = insp.widget("pos_z") w.probe_drag(40.0) # 40 px × 0,05 m/px = +2,0 m var now: float = float(_sel_obj(0).get("pos", [0, 0, 0])[2]) _check(absf(now - (base + 2.0)) < 0.0001, "T3 Ziehen am Label ändert Z um +2,0 m") var marker = main.view_builder.object_markers[0] _check(marker != null and absf(marker.position.z - now) < 0.0001, "T3 3D-Marker folgt live beim Ziehen") _check(_undo_steps_to_clear() == 1, "T3 Ziehen (Loslassen) = GENAU EIN Undo-Schritt") func _t4_mehrfach_drehung() -> void: main.interaction.call("select_many", [0, 1]) await process_frame var w = insp.widget("rot_y") w.edit.text = "45" w.edit.text_submitted.emit("45") var r0: float = float(_sel_obj(0).get("rot_y_deg", 0.0)) var r1: float = float(_sel_obj(1).get("rot_y_deg", 0.0)) _check(absf(r0 - 45.0) < 0.0001 and absf(r1 - 45.0) < 0.0001, "T4 Mehrfachauswahl: beide Drehungen = 45°") _check(_undo_steps_to_clear() == 1, "T4 Mehrfach-Änderung = GENAU EIN Undo-Schritt") _check(absf(float(_sel_obj(0).get("rot_y_deg", 0.0))) < 0.0001 and absf(float(_sel_obj(1).get("rot_y_deg", 0.0)) - 10.0) < 0.0001, "T4 Undo stellt 0°/10° wieder her") func _t5_mixed_angleichen() -> void: main.interaction.call("select_many", [0, 1]) await process_frame var w = insp.widget("pos_x") _check(str(w.edit.text) == "—", "T5 ungleiche X-Werte zeigen „—“") 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") func _t6_groesse() -> void: _select(0) var w = insp.widget("scale") w.edit.text = "100" w.edit.text_submitted.emit("100") _check(absf(float(_sel_obj(0).get("scale", 1.0)) - 20.0) < 0.0001, "T6 Größe auf 100 geklemmt auf 20") var marker = main.view_builder.object_markers[0] var mi: MeshInstance3D = marker.get_child(0) as MeshInstance3D if glb_ok: _check(mi != null and mi.scale.length() > 19.9, "T6 Marker-Mesh skaliert live (echtes Modell)") _check(float(marker.get_meta("half_height", 0.0)) > 0.0 and absf(float(marker.get_meta("pick_radius", 0.0))) > 0.0, "T6 Pick-Metas nach Rescale gesetzt") _check(_undo_steps_to_clear() == 1, "T6 Größen-Tippen = GENAU EIN Undo-Schritt") # Zurücksetzen-Knopf (Undo hat die Auswahl geleert → neu anwählen). _select(0) w = insp.widget("scale") w.edit.text = "3" w.edit.text_submitted.emit("3") _check(absf(float(_sel_obj(0).get("scale", 1.0)) - 3.0) < 0.0001, "T6 Größe 3.0 gesetzt") insp.get("_reset_btn").pressed.emit() _check(absf(float(_sel_obj(0).get("scale", 1.0)) - 1.0) < 0.0001, "T6 Zurücksetzen stellt 1.0 her") # (Vorher: Tippen 3.0 = 1 Schritt, Zurücksetzen = 1 Schritt → EIN Undo # holt das Zurücksetzen zurück.) _check(main.history.can_undo(), "T6 Zurücksetzen erzeugt Undo-Eintrag") main.call("_do_undo") _check(absf(float(_sel_obj(0).get("scale", 1.0)) - 3.0) < 0.0001, "T6 EIN Undo-Schritt holt 3.0 zurück (kein Frame-/Tasten-Splitting)") _undo_steps_to_clear() # Platzhalter-Pfad (crate_wood, scale 2.0 → 1.0) main.interaction.call("select_object", 1) await process_frame var w2 = insp.widget("scale") w2.edit.text = "1" w2.edit.text_submitted.emit("1") _check(absf(float(_sel_obj(1).get("scale", 1.0)) - 1.0) < 0.0001, "T6 Platzhalter-Größe geändert") var marker2 = main.view_builder.object_markers[1] var mi2: MeshInstance3D = marker2.get_child(0) as MeshInstance3D _check(mi2 != null and absf(mi2.scale.x - 0.5) < 0.01, "T6 Platzhalter-Mesh relativ reskaliert (2.0 → 1.0 = Faktor 0.5)") _undo_steps_to_clear() func _t7_auf_boden() -> void: _select(0) var w = insp.widget("pos_y") w.edit.text = "5" w.edit.text_submitted.emit("5") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - 5.0) < 0.0001, "T7 Höhe getippt: y = 5") insp.get("_ground_btn").pressed.emit() var floor_y: float = float(_sel_obj(0).get("pos", [0, 0, 0])[1]) var expected: float = SvmapDocument.terrain_height_at(main.doc, 4.0, 4.0) _check(absf(floor_y - expected) < 0.01, "T7 'Auf Boden setzen' setzt auf Terrain-Höhe") # Zwei Schritte in der History: Tippen (0→5) + Boden (5→Boden). Jedes EIN Schritt. _check(main.history.can_undo(), "T7 Boden-Setzen hat Undo-Eintrag") main.call("_do_undo") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - 5.0) < 0.0001, "T7 1. Undo holt das Boden-Setzen zurück (y = 5)") main.call("_do_undo") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - 0.0) < 0.0001, "T7 2. Undo holt das Tippen zurück (y = 0, Ausgangswert)") _check(not main.history.can_undo(), "T7 genau 2 Undo-Schritte, dann History leer") # Y unter Boden tippen wird geklemmt (nichts begraben). w.edit.text = "-3" w.edit.text_submitted.emit("-3") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - expected) < 0.01, "T7 y unter dem Boden wird auf Terrain-Höhe geklemmt") _undo_steps_to_clear() func _t8_leer() -> void: main.interaction.call("clear_selection") await process_frame _check(insp.get("_hint").visible, "T8 ohne Auswahl: Hinweis sichtbar") _check(not insp.get("_grid").visible, "T8 ohne Auswahl: Felder aus") _check(not insp.get("_ground_btn").visible, "T8 ohne Auswahl: Knöpfe aus") func _finish() -> void: if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1)