scopedvr-map-editor/tests/test_orbit_camera.gd
slyphax 491a47d3f1 Editor-Bibliothek auf das Military-Desert-Pack umgestellt
- 423 Objekte in 19 deutschen Kategorien (Panzer, Radfahrzeuge, Flugzeuge, Gebaeude,
  Zelte, Tuerme, Mauern, Sperren, Kisten, Technik, Felsen, Boden, Decals ...);
  Gruppen "Bauwerke" und "Kisten & Barrieren" samt Test-Asset-Namenslisten entfernt
- LibraryBrowser: Gruppen/Namen aus dem Manifest (kategorie, name_de), unbekannte
  Kategorien bekommen automatisch eine eigene Gruppe
- Thumbs neu, library/ synchronisiert (inkl. neuer Kollision)
- Neue Identitaetsprobe: Kollision ALLER Objekte Editor = Client = Server
- Tests auf Pack-Objekte umgestellt

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-25 16:31:28 +01:00

481 lines
No EOL
20 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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://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)