481 lines
No EOL
20 KiB
GDScript
481 lines
No EOL
20 KiB
GDScript
extends SceneTree
|
||
## Editor-Test (headless): Karte t_950fddaa (K1) — Kamera-Drehen fixen —
|
||
## erweitert um K1b (t_eca890c5, Inhalts-Ansicht) und K1d (t_6c1027c1).
|
||
## Ausführen: godot4 --headless --path . --script res://tests/test_orbit_camera.gd
|
||
## Exit 0 = alles grün.
|
||
##
|
||
## Instanziert die ECHTE Main-UI (wie test_rmb_vorrang) und prüft:
|
||
## 1. STARTANSICHT (K1d): Laden/Neu → fit_to_document — Kamera auf der
|
||
## KARTENMITTE (Mitte der Map-Bounds) mit START_DISTANCE_M = 80 m,
|
||
## yaw 45°, pitch 35°; Statuszeile „Ansicht: Kartenmitte, Abstand 80 m
|
||
## (Pos1 = ganze Karte).“; Startansicht ≠ ganze Karte (Ecken NICHT alle
|
||
## im Frustum). Regel gilt unabhängig von Kartengröße (256/1024/4096)
|
||
## und Inhaltslage (Objekte am Rand ändern nichts); leeres Dokument
|
||
## ebenfalls 80 m (mit „kein Inhalt“ im Status).
|
||
## 2. KEIN SPRUNG: MMT-Druck ändert Position/Basis NICHT (1e-4), aber
|
||
## target = Trefferpunkt; Projektion des target ≈ Cursor (1 px).
|
||
## Vergleichszeile dokumentiert, wie weit das ALTE Verhalten weg war.
|
||
## 3. DREHPUNKT AUF DEM TERRAIN (K1b): MMT auf Hügel-Flanke (höheres
|
||
## Heightfield) → target = Terrain-Treffer, y ≈ terrain_height_at —
|
||
## nicht die Basis-Ebene (y=0) und nicht der alte Ziel-Punkt.
|
||
## 4. GLEICHMÄSSIG: 100 px ziehen in 10 Schritten → jede Schritt dreht
|
||
## gleichmäßig (max 1,3× Mittel), kein Vorzeichenwechsel.
|
||
## 5. FLUG-ÜBERGANG: _enter_fly → Bewegung → _exit_fly ohne Positionssprung.
|
||
## 6. POS1: GANZE Karte wieder im Bild — auch nach Objekt-Ansicht (K1b).
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
var main: Control
|
||
var camera: Camera3D
|
||
var subvp: SubViewport
|
||
var interaction: Node
|
||
var view_builder: Node
|
||
var base_doc: Dictionary # Grund-Dokument der Suite (K1-Szene) für Rückkehr nach Spezial-Gruppen
|
||
|
||
const ORBIT_DEG_PER_PX := 0.3
|
||
|
||
|
||
func _initialize() -> void:
|
||
_run_all.call_deferred()
|
||
|
||
|
||
func _run_all() -> void:
|
||
var scene: PackedScene = load("res://core/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
|
||
|
||
camera = main.get("camera")
|
||
interaction = main.get("interaction")
|
||
view_builder = main.get("view_builder")
|
||
_check(camera != null, "Kamera-Referenz gefunden")
|
||
_check(interaction != null, "interaction-Referenz gefunden")
|
||
_check(view_builder != null, "view_builder-Referenz gefunden")
|
||
if camera == null or interaction == null or view_builder == null:
|
||
_finish()
|
||
return
|
||
var container := _find_viewport_container(main)
|
||
_check(container != null, "SubViewportContainer gefunden")
|
||
if container == null:
|
||
_finish()
|
||
return
|
||
subvp = container.get_child(0) as SubViewport
|
||
_check(subvp != null, "SubViewport gefunden")
|
||
if subvp == null:
|
||
_finish()
|
||
return
|
||
# Headless-Fenster ist degeneriert klein (SubViewport ~951×2 px) — für
|
||
# aussagekräftige Projektionen/Frustum-Prüfungen Editor-Größe erzwingen.
|
||
container.stretch = false
|
||
subvp.size = Vector2i(1600, 900)
|
||
await process_frame
|
||
_check(subvp.size == Vector2i(1600, 900), "SubViewport auf 1600×900 (headless)")
|
||
|
||
# Test-Map anlegen (gleicher Pfad wie „Neue Map“): _refresh_all löst
|
||
# fit_to_document in _refresh_viewport aus (Karte K1d — 80 m auf Kartenmitte).
|
||
var doc: Dictionary = SvmapDocument.create_new("K1-Test", 1024.0, "mittag")
|
||
doc["objects"] = [
|
||
{"id": "box_001", "pos": [0.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "box_001", "pos": [450.0, 0.0, 450.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]
|
||
base_doc = doc
|
||
main.set("doc", doc)
|
||
main.call("_refresh_all")
|
||
await process_frame
|
||
interaction.call("refresh_terrain_cache")
|
||
main.call("_set_tool_mode", "objects")
|
||
await process_frame
|
||
|
||
_test_start_view()
|
||
await _test_pivot_no_jump()
|
||
await _test_content_rules()
|
||
await _test_terrain_pivot()
|
||
await _test_uniform_drag()
|
||
await _test_fly_transition()
|
||
await _test_home_key()
|
||
|
||
_finish()
|
||
|
||
|
||
# ------------------------------------------------- 1. STARTANSICHT (K1d) ---
|
||
func _test_start_view() -> void:
|
||
print("Testgruppe: Startansicht = Kartenmitte, 80 m (K1d, Laden/Neu)")
|
||
var target: Vector3 = camera.get("target")
|
||
var dist: float = float(camera.get("distance"))
|
||
# 1024er Map: Map-Bounds min (−512, −10, −512), max (512, 80, 512)
|
||
# → XZ-Mitte (0, 0); Höhe = Gelände dort (neue Map: 0) — NICHT die
|
||
# y-Mitte der Bounds (35 m), sonst zielt die Ansicht in die Luft.
|
||
# Inhalt (2 Kisten + 8 Default-Spawns) ist für die Startansicht seit K1d
|
||
# IRRELEVANT — Sly will 80 m, nicht 1 km.
|
||
_check(target.distance_to(Vector3(0.0, 0.0, 0.0)) < 1e-4,
|
||
"target = Kartenmitte auf dem Gelände (0 / 0 / 0) — NICHT Content-Mitte, NICHT Bounds-Mitte (35 m)")
|
||
_check(absf(dist - 80.0) < 1e-4,
|
||
"distance = START_DISTANCE_M = 80 m (%.4f) — Slys Wahl „nah dran“" % dist)
|
||
_check(absf(float(camera.get("yaw_deg")) - 45.0) < 1e-4, "yaw = 45 Grad")
|
||
_check(absf(float(camera.get("pitch_deg")) - 35.0) < 1e-4, "pitch = 35 Grad")
|
||
# „80 m über der Kartenmitte“: Blickachse 35° nach unten, also liegt die
|
||
# Kamera 80 × sin(35°) = 45,9 m über der Kartenmitte.
|
||
var hub := camera.position.y - target.y
|
||
_check(absf(hub - 80.0 * sin(deg_to_rad(35.0))) < 1e-3,
|
||
"Kamera %.2f m über der Kartenmitte (= 80 × sin 35°)" % hub)
|
||
_check(absf(camera.position.distance_to(target) - 80.0) < 1e-3,
|
||
"Kamera-Abstand zum Drehpunkt = 80 m (%.4f)" % camera.position.distance_to(target))
|
||
var status: String = str((main.get("status_label") as Label).text)
|
||
_check(status.contains("Ansicht: Kartenmitte") and status.contains("Abstand 80 m")
|
||
and status.contains("Pos1 = ganze Karte"),
|
||
"Statuszeile Klartext: „%s“" % status)
|
||
# Startansicht ist NAH dran → die 1-km-Karte passt NICHT mehr ins Bild.
|
||
var ecken_drin := 0
|
||
for c in _corners(Vector3(-512.0, -10.0, -512.0), Vector3(512.0, 80.0, 512.0)):
|
||
if camera.is_position_in_frustum(c):
|
||
ecken_drin += 1
|
||
_check(ecken_drin < 8,
|
||
"Startansicht ≠ ganze Karte: nur %d/8 Map-Ecken im Frustum (Pos1 zeigt alle 8)" % ecken_drin)
|
||
|
||
|
||
# ------------------------- 1b. 80-m-REGEL: Größe/Inhaltslage/leer (K1d) ---
|
||
func _test_content_rules() -> void:
|
||
print("Testgruppe: 80-m-Regel — unabhängig von Größe und Inhaltslage (K1d)")
|
||
var mn256 := Vector3(-128.0, -10.0, -128.0)
|
||
var mx256 := Vector3(128.0, 80.0, 128.0)
|
||
var KM := Vector3(0.0, 0.0, 0.0) # Kartenmitte (XZ 0/0, Geländehöhe 0)
|
||
# (a) 256-m-Map, KEINE Spawns, 3 Objekte um x 0…40 → trotzdem 80 m
|
||
# auf der KARTENMITTE (Inhalt zieht die Ansicht NICHT mehr weg).
|
||
var doc256: Dictionary = SvmapDocument.create_new("K1d-256", 256.0, "mittag")
|
||
doc256["spawn_zones"] = {}
|
||
doc256["objects"] = [
|
||
{"id": "box_001", "pos": [0.0, 10.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "box_001", "pos": [40.0, 30.0, -10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "box_001", "pos": [20.0, 20.0, 5.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]
|
||
camera.call("fit_to_document", doc256, mn256, mx256)
|
||
var target: Vector3 = camera.get("target")
|
||
var dist: float = float(camera.get("distance"))
|
||
_check(target.distance_to(KM) < 1e-4,
|
||
"256-m-Map: target = Kartenmitte (0 / 0 / 0), nicht Content-Mitte (20 / 20 / −2,5)")
|
||
_check(absf(dist - 80.0) < 1e-4, "256-m-Map: distance = 80 m (%.2f)" % dist)
|
||
|
||
# (b) Inhalt diagonal über die GANZE Karte → immer noch 80 m (kein Deckel
|
||
# mehr, keine Kantenlänge × 1,3).
|
||
doc256["objects"] = [
|
||
{"id": "box_001", "pos": [-128.0, 0.0, -128.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
{"id": "box_001", "pos": [128.0, 0.0, 128.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]
|
||
camera.call("fit_to_document", doc256, mn256, mx256)
|
||
dist = float(camera.get("distance"))
|
||
_check(absf(dist - 80.0) < 1e-4,
|
||
"Inhalt über die ganze Karte: trotzdem 80 m (%.2f) — keine Kanten-Regel mehr" % dist)
|
||
|
||
# (c) einzelnes Objekt abseits der Mitte → 80 m auf der KARTENMITTE
|
||
doc256["objects"] = [
|
||
{"id": "box_001", "pos": [10.0, 5.0, 10.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||
]
|
||
camera.call("fit_to_document", doc256, mn256, mx256)
|
||
target = camera.get("target")
|
||
dist = float(camera.get("distance"))
|
||
_check(target.distance_to(KM) < 1e-4, "1 Objekt: target bleibt Kartenmitte")
|
||
_check(absf(dist - 80.0) < 1e-4, "1 Objekt: distance = 80 m (%.2f) — nicht 40 m" % dist)
|
||
|
||
# (d) leeres Dokument (ohne Default-Spawns!) → ebenfalls 80 m + Klartext
|
||
var doc_leer: Dictionary = SvmapDocument.create_new("K1d-leer", 256.0, "mittag")
|
||
doc_leer["spawn_zones"] = {}
|
||
camera.call("fit_to_document", doc_leer, mn256, mx256)
|
||
target = camera.get("target")
|
||
dist = float(camera.get("distance"))
|
||
_check(target.distance_to(KM) < 1e-4, "leeres Dokument: target = Kartenmitte")
|
||
_check(absf(dist - 80.0) < 1e-4, "leeres Dokument: distance = 80 m (%.2f)" % dist)
|
||
var st_leer: String = str((main.get("status_label") as Label).text)
|
||
_check(st_leer.contains("kein Inhalt") and st_leer.contains("Pos1 = ganze Karte"),
|
||
"leeres Dokument: Status „%s“" % st_leer)
|
||
|
||
# (e) große Karte (4096 m) → auch 80 m (Regel skaliert NICHT mit der Karte)
|
||
var mn4k := Vector3(-2048.0, -10.0, -2048.0)
|
||
var mx4k := Vector3(2048.0, 80.0, 2048.0)
|
||
var doc4k: Dictionary = SvmapDocument.create_new("K1d-4096", 4096.0, "mittag")
|
||
camera.call("fit_to_document", doc4k, mn4k, mx4k)
|
||
target = camera.get("target")
|
||
dist = float(camera.get("distance"))
|
||
_check(target.distance_to(KM) < 1e-4 and absf(dist - 80.0) < 1e-4,
|
||
"4096-m-Map: ebenfalls Kartenmitte + 80 m (%.2f)" % dist)
|
||
|
||
# Test-Dokument der Suite wiederherstellen (Folgegruppen erwarten es).
|
||
main.set("doc", base_doc)
|
||
main.call("_refresh_all")
|
||
await process_frame
|
||
|
||
|
||
# ------------------------------------------ 1c. DREHPUNKT AUF TERRAIN (K1b)
|
||
func _test_terrain_pivot() -> void:
|
||
print("Testgruppe: Drehpunkt trifft das TERRAIN (K1b)")
|
||
# Hügel-Map: Plateau 30 m (r ≤ 150 m), sonst Grundhöhe 5 m — Heightfield
|
||
# R16/LE direkt schreiben (gleiche Kodierung wie SvmapIO.decode_heights).
|
||
var doc: Dictionary = SvmapDocument.create_new("K1b-Huegel", 256.0, "mittag")
|
||
var terrain: Dictionary = doc["terrain"]
|
||
var res := int(terrain["resolution"])
|
||
var hmin := float(terrain["min_height_m"])
|
||
var hmax := float(terrain["max_height_m"])
|
||
var bytes := PackedByteArray()
|
||
bytes.resize(res * res * 2)
|
||
for zi in res:
|
||
for xi in res:
|
||
var x := (float(xi) / float(res - 1) - 0.5) * 256.0
|
||
var z := (float(zi) / float(res - 1) - 0.5) * 256.0
|
||
var h := 30.0 if sqrt(x * x + z * z) <= 150.0 else 5.0
|
||
bytes.encode_u16((zi * res + xi) * 2, int(round((h - hmin) / (hmax - hmin) * 65535.0)))
|
||
terrain["heightmap_r16_b64"] = Marshalls.raw_to_base64(bytes)
|
||
doc["spawn_zones"] = {}
|
||
doc["objects"] = []
|
||
main.set("doc", doc)
|
||
main.call("_refresh_all")
|
||
await process_frame
|
||
interaction.call("refresh_terrain_cache")
|
||
await process_frame
|
||
|
||
var soll_y := SvmapDocument.terrain_height_at(doc, 120.0, 0.0)
|
||
print(" ZAHLEN: terrain_height_at(120, 0) = %.2f m (Plateau 30 m erwartet)" % soll_y)
|
||
var p_welt := Vector3(120.0, 30.0, 0.0) # auf dem Plateau, weit weg von Objekten
|
||
var press: Vector2 = camera.unproject_position(p_welt)
|
||
var pos_before: Vector3 = camera.position
|
||
var basis_before: Basis = camera.global_transform.basis
|
||
_motion(press)
|
||
_mouse(MOUSE_BUTTON_MIDDLE, true, press)
|
||
await process_frame
|
||
|
||
var pos_after: Vector3 = camera.position
|
||
var basis_after: Basis = camera.global_transform.basis
|
||
_check(pos_after.distance_to(pos_before) < 1e-4, "KEIN Sprung: Position identisch")
|
||
_check(_basis_diff(basis_before, basis_after) < 1e-4, "KEIN Sprung: Basis identisch")
|
||
var tgt: Vector3 = camera.get("target")
|
||
_check(tgt.distance_to(p_welt) < 8.0,
|
||
"target = Terrain-Treffer (Abweichung %.2f m zum Sollpunkt)" % tgt.distance_to(p_welt))
|
||
_check(absf(tgt.y - SvmapDocument.terrain_height_at(doc, tgt.x, tgt.z)) < 1.0,
|
||
"target.y ≈ terrain_height_at(target.x, target.z): %.2f vs %.2f" % [
|
||
tgt.y, SvmapDocument.terrain_height_at(doc, tgt.x, tgt.z)])
|
||
_check(tgt.y > 20.0,
|
||
"target liegt AUF dem Hügel (y = %.2f), nicht auf der Basis-Ebene (0 m)" % tgt.y)
|
||
var proj: Vector2 = camera.unproject_position(tgt)
|
||
_check(proj.distance_to(press) <= 2.0,
|
||
"target-Projektion ≈ Cursor (%.2f px)" % proj.distance_to(press))
|
||
_mouse(MOUSE_BUTTON_MIDDLE, false, press)
|
||
await process_frame
|
||
|
||
# Grund-Dokument zurück (Pos1-Gruppe braucht die 1024er-Map-Bounds).
|
||
main.set("doc", base_doc)
|
||
main.call("_refresh_all")
|
||
await process_frame
|
||
interaction.call("refresh_terrain_cache")
|
||
await process_frame
|
||
|
||
|
||
# -------------------------------------------- 2. PIVOT UNTER DEM CURSOR -----
|
||
func _test_pivot_no_jump() -> void:
|
||
print("Testgruppe: Drehpunkt unter dem Mauszeiger (kein Sprung)")
|
||
var markers: Array = view_builder.get("object_markers")
|
||
# K1d: Die Startansicht steht 80 m über der Kartenmitte — sichtbar ist der
|
||
# Nahbereich, also der Marker an (0 / 0 / 0) (der zweite liegt bei
|
||
# 450 / 450 und wäre außerhalb des Bildes; ein Klick dort träfe nichts).
|
||
var marker: Node3D = markers[0]
|
||
_check(marker != null and is_instance_valid(marker),
|
||
"Objekt-Marker vorhanden (box_001 bei 0, 0)")
|
||
if marker == null:
|
||
return
|
||
var center: Vector3 = marker.global_position \
|
||
+ Vector3.UP * float(marker.get_meta("half_height", 0.0))
|
||
var press: Vector2 = camera.unproject_position(center)
|
||
_check(subvp.get_visible_rect().has_point(press),
|
||
"Objekt liegt in der Startansicht im Bild (Cursor %.0f / %.0f)" % [press.x, press.y])
|
||
# Altes Verhalten dokumentieren: Drehpunkt war der (35 m höhere) Ziel-Punkt
|
||
# der Ansicht — seine Projektion lag weit weg vom Cursor.
|
||
var alt_target: Vector3 = camera.get("target")
|
||
var alt_px: Vector2 = camera.unproject_position(alt_target)
|
||
print(" ALT (Vergleich, alter Code): Drehpunkt-Projektion lag %.0f px vom Cursor entfernt" % alt_px.distance_to(press))
|
||
|
||
var pos_before: Vector3 = camera.position
|
||
var basis_before: Basis = camera.global_transform.basis
|
||
|
||
_motion(press)
|
||
_mouse(MOUSE_BUTTON_MIDDLE, true, press)
|
||
await process_frame
|
||
|
||
var pos_after: Vector3 = camera.position
|
||
var basis_after: Basis = camera.global_transform.basis
|
||
_check(pos_after.distance_to(pos_before) < 1e-4,
|
||
"KEIN Sprung: Kameraposition identisch (Abweichung %.12f)" % pos_after.distance_to(pos_before))
|
||
_check(_basis_diff(basis_before, basis_after) < 1e-4,
|
||
"KEIN Sprung: Basis identisch")
|
||
var new_target: Vector3 = camera.get("target")
|
||
_check(new_target.distance_to(center) < 1e-4, "target = Trefferpunkt (Objekt-Mitte)")
|
||
var proj: Vector2 = camera.unproject_position(new_target)
|
||
_check(proj.distance_to(press) <= 1.0,
|
||
"target-Projektion ≈ Cursor (Abweichung %.12f px)" % proj.distance_to(press))
|
||
var new_dist: float = float(camera.get("distance"))
|
||
_check(absf(new_dist - pos_before.distance_to(center)) < 1e-3,
|
||
"distance = |Kamera − Treffer| (%.1f m)" % new_dist)
|
||
_mouse(MOUSE_BUTTON_MIDDLE, false, press)
|
||
await process_frame
|
||
|
||
|
||
# ------------------------------------------------- 3. GLEICHMÄSSIG ZIEHEN --
|
||
func _test_uniform_drag() -> void:
|
||
print("Testgruppe: 100 px ziehen in 10 Schritten — gleichmäßig")
|
||
var start := subvp.size * 0.5
|
||
_motion(start)
|
||
_mouse(MOUSE_BUTTON_MIDDLE, true, start)
|
||
await process_frame
|
||
var yaw0: float = float(camera.get("yaw_deg"))
|
||
var steps: Array[float] = []
|
||
var p := start
|
||
for i in range(10):
|
||
p.x += 10.0
|
||
_motion_rel(p, Vector2(10.0, 0.0))
|
||
await process_frame
|
||
var yaw_i: float = float(camera.get("yaw_deg"))
|
||
steps.append(yaw0 - yaw_i)
|
||
yaw0 = yaw_i
|
||
_mouse(MOUSE_BUTTON_MIDDLE, false, p)
|
||
await process_frame
|
||
var mean := 0.0
|
||
for s in steps:
|
||
mean += s
|
||
mean /= float(steps.size())
|
||
_check(absf(mean - 3.0) < 1e-3, "Mittel: %.3f Grad/Schritt (= 10 px × %.1f)" % [mean, ORBIT_DEG_PER_PX])
|
||
var max_ratio := 0.0
|
||
var sign_ok := true
|
||
for s in steps:
|
||
if mean > 0.0:
|
||
max_ratio = maxf(max_ratio, s / mean)
|
||
if s <= 0.0:
|
||
sign_ok = false
|
||
_check(max_ratio <= 1.3, "kein Nachlaufen/Sprung: max Schritt %.3f× Mittel" % max_ratio)
|
||
_check(sign_ok, "kein Vorzeichenwechsel beim Ziehen")
|
||
|
||
|
||
# ------------------------------------------------------ 4. FLUG-ÜBERGANG ---
|
||
func _test_fly_transition() -> void:
|
||
print("Testgruppe: Flug-Übergang nahtlos")
|
||
_mouse(MOUSE_BUTTON_RIGHT, true, subvp.size * 0.5)
|
||
await process_frame
|
||
_check(bool(camera.call("fly_active")), "Flug aktiv")
|
||
# Zwischenbewegung (wie im Flug: Position + Blickrichtung ändern).
|
||
camera.position = camera.position + Vector3(12.0, -4.0, -9.0)
|
||
camera.set("_fly_yaw", -30.0)
|
||
camera.set("_fly_pitch", 20.0)
|
||
camera.call("_apply_fly_orientation")
|
||
await process_frame
|
||
var pos_before: Vector3 = camera.position
|
||
_mouse(MOUSE_BUTTON_RIGHT, false, subvp.size * 0.5)
|
||
await process_frame
|
||
_check(not bool(camera.call("fly_active")), "Flug aus → Orbit")
|
||
var pos_after: Vector3 = camera.position
|
||
_check(pos_after.distance_to(pos_before) < 1e-3,
|
||
"kein Positionssprung beim Übergang (%.12f)" % pos_after.distance_to(pos_before))
|
||
|
||
|
||
# ------------------------------------------------------------- 5. POS1 -----
|
||
func _test_home_key() -> void:
|
||
print("Testgruppe: Pos1 = ganze Karte ins Bild (≠ Startansicht, K1d)")
|
||
# Vorher bewusst in die Startansicht (80 m) zurück — Pos1 muss sie verlassen.
|
||
camera.call("fit_to_document", base_doc, Vector3(-512.0, -10.0, -512.0),
|
||
Vector3(512.0, 80.0, 512.0))
|
||
var dist_start: float = float(camera.get("distance"))
|
||
var key := InputEventKey.new()
|
||
key.keycode = KEY_HOME
|
||
key.physical_keycode = KEY_HOME
|
||
key.pressed = true
|
||
subvp.push_input(key)
|
||
await process_frame
|
||
var target: Vector3 = camera.get("target")
|
||
_check(target.distance_to(Vector3(0.0, 35.0, 0.0)) < 1e-4, "Pos1: target = Kartemitte")
|
||
var radius := Vector3(1024.0, 90.0, 1024.0).length() * 0.5
|
||
var dist_home: float = float(camera.get("distance"))
|
||
_check(dist_home >= radius / sin(deg_to_rad(37.5)) - 1.0,
|
||
"Pos1: distance = Umkugel-Formel (ganze Map sichtbar)")
|
||
# K1d: Startansicht (80 m) und Pos1 sind ZWEI verschiedene Ansichten —
|
||
# Pos1 zieht bewusst wieder weit heraus.
|
||
_check(absf(dist_start - 80.0) < 1e-4 and dist_home > dist_start * 2.0,
|
||
"Pos1 ≠ Startansicht: 80 m → %.1f m (Faktor %.1f)" % [dist_home, dist_home / maxf(dist_start, 0.001)])
|
||
_check(str((main.get("status_label") as Label).text).contains("ganze Karte"),
|
||
"Pos1: Status meldet „ganze Karte“ („%s“)" % str((main.get("status_label") as Label).text))
|
||
_check(absf(float(camera.get("yaw_deg")) - 45.0) < 1e-4, "Pos1: yaw = 45 Grad")
|
||
_check(absf(float(camera.get("pitch_deg")) - 35.0) < 1e-4, "Pos1: pitch = 35 Grad")
|
||
var alle_drin := true
|
||
for c in _corners(Vector3(-512.0, -10.0, -512.0), Vector3(512.0, 80.0, 512.0)):
|
||
if not camera.is_position_in_frustum(c):
|
||
alle_drin = false
|
||
_check(alle_drin, "Pos1: alle 8 Map-Ecken im Frustum")
|
||
|
||
|
||
# ------------------------------------------------------------- Helpers -----
|
||
func _corners(mn: Vector3, mx: Vector3) -> Array[Vector3]:
|
||
var out: Array[Vector3] = []
|
||
for x in [mn.x, mx.x]:
|
||
for y in [mn.y, mx.y]:
|
||
for z in [mn.z, mx.z]:
|
||
out.append(Vector3(x, y, z))
|
||
return out
|
||
|
||
|
||
func _basis_diff(a: Basis, b: Basis) -> float:
|
||
var d := 0.0
|
||
d = maxf(d, (a.x - b.x).length())
|
||
d = maxf(d, (a.y - b.y).length())
|
||
d = maxf(d, (a.z - b.z).length())
|
||
return d
|
||
|
||
|
||
func _find_viewport_container(node: Node) -> SubViewportContainer:
|
||
if node is SubViewportContainer:
|
||
return node as SubViewportContainer
|
||
for child in node.get_children():
|
||
var found := _find_viewport_container(child)
|
||
if found != null:
|
||
return found
|
||
return null
|
||
|
||
|
||
## Headless-Besonderheit (wie test_rmb_vorrang): direkt in den SubViewport
|
||
## pushen, LOKALE Koordinaten.
|
||
func _mouse(button: MouseButton, pressed: bool, pos: Vector2, ctrl := false, shift := false) -> void:
|
||
var mb := InputEventMouseButton.new()
|
||
mb.button_index = button
|
||
mb.pressed = pressed
|
||
mb.position = pos
|
||
mb.global_position = pos
|
||
mb.ctrl_pressed = ctrl
|
||
mb.shift_pressed = shift
|
||
subvp.push_input(mb)
|
||
|
||
|
||
func _motion(pos: Vector2) -> void:
|
||
_motion_rel(pos, Vector2.ZERO)
|
||
|
||
|
||
func _motion_rel(pos: Vector2, rel: Vector2) -> void:
|
||
var mm := InputEventMouseMotion.new()
|
||
mm.position = pos
|
||
mm.global_position = pos
|
||
mm.relative = rel
|
||
subvp.push_input(mm)
|
||
|
||
|
||
func _finish() -> void:
|
||
if failures == 0:
|
||
print("ALL OK: %d checks" % checks)
|
||
quit(0)
|
||
else:
|
||
print("FAILED: %d/%d checks" % [failures, checks])
|
||
quit(1)
|
||
|
||
|
||
func _check(cond: bool, label: String) -> void:
|
||
checks += 1
|
||
if cond:
|
||
print(" ok: %s" % label)
|
||
else:
|
||
failures += 1
|
||
print(" FAIL: %s" % label) |