From 2c5b58f04c26a0b5ff275d175de09e6630ffe76a Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Tue, 20 Oct 2026 07:09:11 +0000 Subject: [PATCH] =?UTF-8?q?t=5F950fddaa=20K1:=20Kamera-Drehen=20fixen=20?= =?UTF-8?q?=E2=80=94=20Drehpunkt=20unter=20den=20Mauszeiger,=20Map=20beim?= =?UTF-8?q?=20Laden=20passend=20einpassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/ui/main.gd | 18 +- src/viewport/orbit_camera.gd | 142 ++++++++++++++- tests/test_orbit_camera.gd | 308 +++++++++++++++++++++++++++++++++ tests/test_orbit_camera.gd.uid | 1 + 4 files changed, 460 insertions(+), 9 deletions(-) create mode 100644 tests/test_orbit_camera.gd create mode 100644 tests/test_orbit_camera.gd.uid diff --git a/src/ui/main.gd b/src/ui/main.gd index 6f34520..7a05220 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -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) ------------------------------------- diff --git a/src/viewport/orbit_camera.gd b/src/viewport/orbit_camera.gd index 43e63c3..0d72378 100644 --- a/src/viewport/orbit_camera.gd +++ b/src/viewport/orbit_camera.gd @@ -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) diff --git a/tests/test_orbit_camera.gd b/tests/test_orbit_camera.gd new file mode 100644 index 0000000..517f8da --- /dev/null +++ b/tests/test_orbit_camera.gd @@ -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) \ No newline at end of file diff --git a/tests/test_orbit_camera.gd.uid b/tests/test_orbit_camera.gd.uid new file mode 100644 index 0000000..1ac6a3f --- /dev/null +++ b/tests/test_orbit_camera.gd.uid @@ -0,0 +1 @@ +uid://o7mjux7sfh85