t_950fddaa K1: Kamera-Drehen fixen — Drehpunkt unter den Mauszeiger, Map beim Laden passend einpassen

orbit_camera.gd: set_pivot(hit) rechnet aus unveränderter Position/Basis
target/yaw/pitch/distance neu (kein Sprung); MMT-Druck pivoted auf den
Punkt unterm Cursor — Objekt-Treffer zuerst (interaction._pick_object),
sonst Terrain (interaction._ground_point), sonst alter Ziel-Punkt.
ORBIT_DEG_PER_PX = 0.3 gekapselt, Zoom-Minimum DISTANCE_MIN = 5.
fit_to_bounds(mn, mx) ersetzt das tote frame_map(): target = Kartemitte
aus bounds, Abstand über Umkugel-Formel (alle 8 Ecken sicher im Frustum —
reiner Kanten-Faktor 0,65 reicht bei flachen Maps nicht), Mindestabstand
30 m; Pos1/Home = ganze Karte ins Bild (Bounds bleiben gemerkt).
main.gd: Laden/Neu + Resize rufen fit_to_bounds mit den echten doc-Bounds.
Neu: tests/test_orbit_camera.gd (30 checks: kein Sprung, Drehpunkt wandert,
fit, gleichmäßiges Ziehen, Flug-Übergang, Pos1).
This commit is contained in:
Hermes (scopedvr-dev) 2026-10-20 07:09:11 +00:00
parent dca0947a71
commit 2c5b58f04c
4 changed files with 460 additions and 9 deletions

View file

@ -1476,7 +1476,14 @@ func _on_resize_confirmed() -> void:
history.commit("Kartengröße %d × %d m" % [int(new_x), int(new_z)],
before, history.snapshot(doc.keys()))
_after_structure_change()
camera.call("frame_map", maxf(new_x, new_z))
# Karte K1 (t_950fddaa): nach Resize die GANZE Karte passend einpassen
# (Kartenmitte aus bounds — frame_map ist weg).
var rb: Dictionary = doc.get("bounds", {})
var rmn: Array = rb.get("min", [])
var rmx: Array = rb.get("max", [])
if rmn.size() == 3 and rmx.size() == 3:
camera.call("fit_to_bounds",
Vector3(rmn[0], rmn[1], rmn[2]), Vector3(rmx[0], rmx[1], rmx[2]))
var old_sx := float(old_terrain.get("size_x_m", 0.0))
var old_sz := float(old_terrain.get("size_z_m", 0.0))
var verb := "verkleinert" if (new_x < old_sx or new_z < old_sz) else "vergrößert"
@ -2896,7 +2903,14 @@ func _refresh_viewport() -> void:
view_builder.call("rebuild", doc, manifest)
_sync_grid_to_builder()
_sync_contours_to_builder()
camera.call("frame_map", float(doc.get("terrain", {}).get("size_x_m", 1024.0)))
# Karte K1 (t_950fddaa): Laden/Neu = ganze Map ins Bild — Kartenmitte aus
# bounds (nicht Weltmitte), Abstand passend zur größten Kantenlänge.
var fb: Dictionary = doc.get("bounds", {})
var fmn: Array = fb.get("min", [])
var fmx: Array = fb.get("max", [])
if fmn.size() == 3 and fmx.size() == 3:
camera.call("fit_to_bounds",
Vector3(fmn[0], fmn[1], fmn[2]), Vector3(fmx[0], fmx[1], fmx[2]))
# --- Maßstabsgitter (Karte t_7ac70014) -------------------------------------

View file

@ -21,8 +21,19 @@ extends Camera3D
## Die Mathe lebt in src/editor/fly_math.gd (pure, headless testbar).
## Ziel-Punkt, Winkel und Abstand bleiben intern; Position wird jede Frame
## aus ihnen berechnet (Orbit) bzw. im Flug direkt bewegt.
## Karte K1 (t_950fddaa): Beim Dreh-Start liegt der Drehpunkt UNTER DEM
## MAUSZEIGER (Unity/Blender-Verhalten) — Objekt/Bauteil-Treffer zuerst, sonst
## Terrain, sonst alter Ziel-Punkt. Kameraposition und Blickrichtung bleiben
## exakt unverändert (kein Sprung), nur Ziel/Winkel/Abstand werden neu
## gerechnet. fit_to_bounds() ersetzt frame_map(): ganze Map ins Bild.
const FlyMath := preload("res://src/editor/fly_math.gd")
const TerrainField := preload("res://src/svmap/terrain_field.gd")
const SvmapIO := preload("res://src/svmap/svmap_io.gd")
const ORBIT_DEG_PER_PX := 0.3 # Karte K1: Drehgeschwindigkeit gekapselt — kein Nachlaufen
const DISTANCE_MIN := 5.0
const FIT_FLOOR_MIN := 30.0 # fit_to_bounds: nie näher als 30 m an die Map ran
var target := Vector3.ZERO
var yaw_deg := 45.0
@ -45,6 +56,9 @@ var _fly_locked := false # Umschalt+F: Flug dauerhaft (auch ohne RMT)
var _flying := false # Ergebnis-Flag: gerade im Flugmodus?
var _fly_yaw := 0.0
var _fly_pitch := 0.0
var _fit_bounds_mn := Vector3.ZERO # zuletzt gesetzte Map-Bounds (fit_to_bounds)
var _fit_bounds_mx := Vector3.ZERO
var _fit_bounds_valid := false
func _ready() -> void:
@ -69,6 +83,9 @@ func _process(delta: float) -> void:
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey:
# Karte K1: Pos1/Home = ganze Karte ins Bild (fit_to_bounds).
_handle_home_key(event as InputEventKey)
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_RIGHT:
@ -85,6 +102,10 @@ func _unhandled_input(event: InputEvent) -> void:
elif mb.button_index == MOUSE_BUTTON_MIDDLE:
if _flying:
return # Im Flug: MMT-Ziehen bleibt unangetastet (kein Orbit-Sprung)
if mb.pressed:
# Karte K1: Drehpunkt unter den Mauszeiger legen — VOR dem
# ersten Ziehen, damit kein Sprung passiert.
_pivot_to_cursor(mb.position)
_dragging = mb.pressed and not mb.shift_pressed
_panning = mb.pressed and mb.shift_pressed
elif mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
@ -94,7 +115,7 @@ func _unhandled_input(event: InputEvent) -> void:
return
if _wheel_blocked():
return
distance = maxf(5.0, distance * 0.9)
distance = maxf(DISTANCE_MIN, distance * 0.9)
_apply()
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
if _flying:
@ -116,8 +137,8 @@ func _unhandled_input(event: InputEvent) -> void:
_apply_fly_orientation()
return
if _dragging:
yaw_deg = wrapf(yaw_deg - mm.relative.x * 0.3, -180.0, 180.0)
pitch_deg = clampf(pitch_deg + mm.relative.y * 0.3, 5.0, 85.0)
yaw_deg = wrapf(yaw_deg - mm.relative.x * ORBIT_DEG_PER_PX, -180.0, 180.0)
pitch_deg = clampf(pitch_deg + mm.relative.y * ORBIT_DEG_PER_PX, 5.0, 85.0)
_apply()
elif _panning:
var right := transform.basis.x
@ -216,18 +237,125 @@ func _apply_fly_orientation() -> void:
look_at_from_position(position, position + fwd, Vector3.UP)
## Kamera auf die Map zentrieren (nach Laden/Neu) — Flugmodus dabei sauber beenden.
func frame_map(size_m: float) -> void:
## Karte K1 (t_950fddaa): Drehpunkt unter den Mauszeiger — aus der unver-
## änderten Kameraposition werden Ziel/Winkel/Abstand neu gerechnet.
func set_pivot(hit: Vector3) -> void:
var pos := position
var fwd := -transform.basis.z
if fwd.length_squared() < 0.0000001:
fwd = FlyMath.forward_from_euler(yaw_deg, pitch_deg)
var ep := FlyMath.euler_from_forward(fwd.normalized())
target = hit
distance = pos.distance_to(hit)
yaw_deg = ep.x
pitch_deg = ep.y
# Bewusst KEIN _apply(): Position und Basis bleiben exakt wie vorher —
# kein Sprung. Der nächste Dreh-/Zoom-Schritt rechnet aus dem neuen
# Zustand dieselbe Position zurück (Offset-Konvention konsistent).
## MMT-Druck: Punkt unter dem Cursor bestimmen — Objekt/Bauteil zuerst, sonst
## Terrain/Boden, sonst aktuellen Ziel-Punkt behalten (kein Sprung).
func _pivot_to_cursor(viewport_pos: Vector2) -> void:
var inter := _find_sibling("_pick_object")
if inter != null:
var idx := int(inter.call("_pick_object", viewport_pos))
if idx >= 0:
var pt: Variant = _object_pivot_point(idx)
if pt != null:
set_pivot(pt)
return
var g: Variant = inter.call("_ground_point", viewport_pos)
if g != null:
set_pivot(g)
return
# Kein Treffer: Ziel-Punkt unverändert lassen — Ansicht darf nicht springen.
## Mittelpunkt des Objekt-Markers (Fußpunkt + halbe Höhe — gleiche Konvention
## wie interaction.gd beim F-Tasten-Zentrieren).
func _object_pivot_point(idx: int) -> Variant:
var builder := _find_builder()
if builder == null:
return null
var markers: Array = builder.get("object_markers")
if markers == null or idx < 0 or idx >= (markers as Array).size():
return null
var marker = (markers as Array)[idx]
if marker == null or not is_instance_valid(marker):
return null
var half_h := float(marker.get_meta("half_height", 0.0))
return (marker as Node3D).global_position + Vector3.UP * half_h
func _find_sibling(has_method_name: String) -> Node:
var p := get_parent()
if p == null:
return null
for child in p.get_children():
var c := child as Node
if c != self and c.has_method(has_method_name):
return c
return null
func _find_builder() -> Node:
var p := get_parent()
if p == null:
return null
for child in p.get_children():
var c := child as Node
if c != self and c.get("object_markers") != null:
return c
return null
## Fit: target = Mitte der bounds; Abstand so, dass die GANZE Map im Frustum
## liegt; Pos1 ruft das mit den zuletzt gesetzten Bounds erneut auf. Ersetzt
## das tote frame_map().
func fit_to_bounds(mn: Vector3, mx: Vector3) -> void:
_fit_bounds_mn = Vector3(minf(mn.x, mx.x), minf(mn.y, mx.y), minf(mn.z, mx.z))
_fit_bounds_mx = Vector3(maxf(mn.x, mx.x), maxf(mn.y, mx.y), maxf(mn.z, mx.z))
_fit_bounds_valid = true
_fly_held = false
_fly_locked = false
_flying = false
target = Vector3.ZERO
distance = maxf(80.0, size_m * 0.6)
var span := _fit_bounds_mx - _fit_bounds_mn
target = _fit_bounds_mn + span * 0.5
# Karte K1: Abstand über Umkugel-Formel — ALLE 8 Ecken sicher im Bild.
# (Reiner Faktor der Kantenlänge reicht bei flachen Maps nicht: die nahe
# Oberecke liegt dann fast seitlich zur Blickachse.)
distance = clampf(_fit_distance(span.length() * 0.5),
FIT_FLOOR_MIN, distance_max())
yaw_deg = 45.0
pitch_deg = 35.0
_apply()
## Abstand, damit die Umkugel (Radius um den Ziel-Punkt) komplett sichtbar
## bleibt: d = R / sin(halber FOV der SCHMALEREN Bildachse) + 2 % Reserve.
func _fit_distance(radius: float) -> float:
var vfov_half := deg_to_rad(fov * 0.5) # fov ist vertikal (KEEP_HEIGHT)
var aspect := 16.0 / 9.0
var vp := get_viewport()
if vp != null:
var vs := vp.get_visible_rect().size
if vs.y > 0.0:
aspect = vs.x / vs.y
var hfov_half := atan(aspect * tan(vfov_half))
var half := minf(vfov_half, hfov_half)
if half <= 0.0 or half >= PI * 0.5:
half = deg_to_rad(37.5)
return maxf(radius, 1.0) / sin(half) * 1.02
func _handle_home_key(key: InputEventKey) -> void:
if key.pressed and not key.echo and key.keycode == KEY_HOME:
if _fit_bounds_valid:
fit_to_bounds(_fit_bounds_mn, _fit_bounds_mx)
_status("Pos1: ganze Karte ins Bild.")
func _apply() -> void:
var pitch := deg_to_rad(pitch_deg)
var yaw := deg_to_rad(yaw_deg)

308
tests/test_orbit_camera.gd Normal file
View file

@ -0,0 +1,308 @@
extends SceneTree
## Editor-Test (headless): Karte t_950fddaa (K1) — Kamera-Drehen fixen.
## 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. FIT: Laden/Neu → fit_to_bounds — target = Kartemitte aus bounds,
## distance = 0,65 × größte Kante, alle 8 Map-Ecken im Frustum.
## 2. KEIN SPRUNG: MMT-Druck ändert Position/Basis NICHT (1e-4), aber
## target = Trefferpunkt; Projektion des target ≈ Cursor (1 px).
## Vergleichszeite dokumentiert, wie weit das ALTE Verhalten weg war.
## 3. GLEICHMÄSSIG: 100 px ziehen in 10 Schritten → jede Schritt dreht
## gleichmäßig (max 1,3× Mittel), kein Vorzeichenwechsel.
## 4. FLUG-ÜBERGANG: _enter_fly → Bewegung → _exit_fly ohne Positionssprung.
## 5. POS1: ganze Karte wieder im Bild (fit_to_bounds).
var failures := 0
var checks := 0
var main: Control
var camera: Camera3D
var subvp: SubViewport
var interaction: Node
var view_builder: Node
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_bounds in _refresh_viewport aus (Karte K1).
var doc: Dictionary = SvmapDocument.create_new("K1-Test", 1024.0, "mittag")
doc["objects"] = [
{"id": "crate_wood", "pos": [0.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
{"id": "crate_wood", "pos": [450.0, 0.0, 450.0], "scale": 1.0, "rot_y_deg": 0.0},
]
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_fit()
await _test_pivot_no_jump()
await _test_uniform_drag()
await _test_fly_transition()
await _test_home_key()
_finish()
# ------------------------------------------------------------- 1. FIT ------
func _test_fit() -> void:
print("Testgruppe: fit_to_bounds (Laden/Neu)")
var target: Vector3 = camera.get("target")
var dist: float = float(camera.get("distance"))
_check(target.distance_to(Vector3(0.0, 35.0, 0.0)) < 1e-4,
"target = Kartemitte aus bounds (0, 35, 0) — nicht Weltmitte y=0")
var radius := Vector3(1024.0, 90.0, 1024.0).length() * 0.5
_check(dist >= radius / sin(deg_to_rad(37.5)) - 1.0,
"distance = Umkugel-Formel (ganze Map sichtbar): %.1f m" % 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")
var mn := Vector3(-512.0, -10.0, -512.0)
var mx := Vector3(512.0, 80.0, 512.0)
var corners: Array[Vector3] = _corners(mn, mx)
var alle_drin := true
for c in corners:
if not camera.is_position_in_frustum(c):
alle_drin = false
_check(alle_drin, "alle 8 Map-Ecken im Frustum")
var editor := camera.unproject_position(target)
var mitte := subvp.size * 0.5
_check(editor.distance_to(mitte) < 1.0,
"Kartemitte projiziert ≈ Bildmitte (%.1f px Abweichung)" % editor.distance_to(mitte))
# -------------------------------------------- 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")
var marker: Node3D = markers[1] # Objekt WEIT abseits der Kartenmitte
_check(marker != null and is_instance_valid(marker),
"Objekt-Marker vorhanden (crate_wood bei 450, 450)")
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)
# Altes Verhalten dokumentieren: Drehpunkt war der Fit-Ziel-Punkt — seine
# Projektion lag weit weg vom Cursor.
var alt_target: Vector3 = Vector3(0.0, 35.0, 0.0)
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")
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
_check(float(camera.get("distance")) >= radius / sin(deg_to_rad(37.5)) - 1.0,
"Pos1: distance = Umkugel-Formel (ganze Map sichtbar)")
_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)

View file

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