Trying to turn a node3d to the left in discrete steps (here 45 degs) so that it just keeps going around. However, it gets to a certain angle (usually just over 180 deg) and then starts to come backwards! Can anyone enlighten me, I am going quite mad.

(I am using Quaternions on advice and my final use-case is RigidBody3D, but until I can get predictable angles and behaviour of them, I best keep it simple.)

Image to show the scene and code. Code also below. It’s on the %pointy node. %aim is what I lookat at, which is always 45 degs.

extends Node3D
func _physics_process(delta: float) -> void:
turn(delta)
func turn(delta):
var currQ := basis.get_rotation_quaternion()
var curr_angle := currQ.get_angle()
# Let's keep adding some angle to current's angle
var toQ := Quaternion(Vector3.UP, currQ.get_angle() + PI/8)
# find the difference - I hope. Who knows?
var diffQ := toQ.normalized() * currQ.inverse()
var diff_angle:= diffQ.get_angle()
print(" curr's angle:", rad_to_deg(curr_angle))
print(" diffQ's ANGLE:", rad_to_deg( diff_angle))
print(" SUM:", rad_to_deg(curr_angle + diff_angle ))
print(" TO angle:", rad_to_deg( toQ.get_angle()), " SHOULD EQUAL SUM")
print()
%pointy.rotate_y(diff_angle)
assert(is_equal_approx(diff_angle, PI/8),"STOP")

Let it run and it will stop when the numbers get funky. Then go look at the output:

diffQ's ANGLE:22.4999476921535
SUM:22.4999476921535
TO angle:22.5000177015925 SHOULD EQUAL SUM
curr's angle:22.4999476921535
diffQ's ANGLE:22.4999476921535
SUM:44.9998953843069
TO angle:44.9999534409149 SHOULD EQUAL SUM
curr's angle:44.9999022144961
diffQ's ANGLE:22.5000177015925
SUM:67.4999199160886
TO angle:67.499904548163 SHOULD EQUAL SUM
curr's angle:67.4999182085413
diffQ's ANGLE:22.5000177015925
SUM:89.9999359101338
TO angle:89.9999273723973 SHOULD EQUAL SUM
curr's angle:89.9999342025865
diffQ's ANGLE:22.5000177015925
SUM:112.499951904179
TO angle:112.499943366442 SHOULD EQUAL SUM
curr's angle:112.499957026821
diffQ's ANGLE:22.5000177015925
SUM:134.999974728413
TO angle:134.999959360488 SHOULD EQUAL SUM
curr's angle:134.999973020866
diffQ's ANGLE:22.4999818430993
SUM:157.499954863965
TO angle:157.499968524344 SHOULD EQUAL SUM
curr's angle:157.499954863965
diffQ's ANGLE:22.5000177015925
SUM:179.999972565558
TO angle:179.999950367443 SHOULD EQUAL SUM
curr's angle:179.999964027821
diffQ's ANGLE:22.4999818430993
SUM:202.499945870921
TO angle:202.499959531299 SHOULD EQUAL SUM
curr's angle:202.499945870921
diffQ's ANGLE:22.4999818430993
SUM:224.99992771402
TO angle:224.999941374398 SHOULD EQUAL SUM
curr's angle:224.99992771402
diffQ's ANGLE:22.4999476921535
SUM:247.499875406173
TO angle:247.499936877876 SHOULD EQUAL SUM
(Here we are at angle 247 degrees, but ...
suddenly it reads as 112 degrees?
Which is maybe why the movement goes wrong.)
curr's angle:112.500120951361
diffQ's ANGLE:247.500237406199
SUM:360.00035835756
TO angle:135.000123285028 SHOULD EQUAL SUM
--- Debugging process stopped ---

A small comment. I have been hacking-away slowly. Here’s some code that seems to work much better. At least the fish does not stop and bounce backwards:

func follow(rotation_force, delta) -> Vector3:
var g := get_gravity()
var negg: = -g.normalized()
var up := negg
var target : Vector3 = transform * %aim.position
var to_basis: Basis
to_basis = transform.looking_at(target, up).basis.orthonormalized()
# !! Doing this multiply mambo with basis works better than with quaternions!
var Q := (to_basis * basis.inverse()).get_rotation_quaternion()
# !!
var axis := Q.get_axis()
var angle := Q.get_angle()
var T = axis * angle * rotation_force * delta
return T