yostlabs 2025.10.29.2__tar.gz → 2026.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yostlabs-2026.2.4/Examples/example_commands.py +33 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/PKG-INFO +1 -1
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/pyproject.toml +1 -1
- yostlabs-2026.2.4/src/yostlabs/math/quaternion.py +354 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/math/vector.py +3 -2
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/api.py +66 -1
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/consts.py +2 -0
- yostlabs-2025.10.29.2/Examples/example_commands.py +0 -26
- yostlabs-2025.10.29.2/src/yostlabs/math/quaternion.py +0 -187
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/.gitignore +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/embedded_2024_dec_20.xml +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_ble.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_component_specific_settings_and_commands.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_firmware_upload.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_parsing_stored_binary.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_read_settings.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_streaming.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_streaming_manager.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/Examples/example_write_settings.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/LICENSE +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/README.md +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/__init__.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/communication/__init__.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/communication/base.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/communication/ble.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/communication/bluetooth.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/communication/serial.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/communication/socket.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/math/__init__.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/__init__.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/eepts.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/utils/__init__.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/utils/calibration.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/utils/parser.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/utils/streaming.py +0 -0
- {yostlabs-2025.10.29.2 → yostlabs-2026.2.4}/src/yostlabs/tss3/utils/version.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from yostlabs.tss3.api import ThreespaceSensor
|
|
2
|
+
from yostlabs.communication.serial import ThreespaceSerialComClass
|
|
3
|
+
|
|
4
|
+
#Create a sensor by auto detecting a ThreespaceSerialComClass
|
|
5
|
+
sensor = ThreespaceSensor(ThreespaceSerialComClass)
|
|
6
|
+
|
|
7
|
+
result = sensor.getPrimaryCorrectedAccelVec()
|
|
8
|
+
|
|
9
|
+
#Read out the result
|
|
10
|
+
accel_vec = result.data
|
|
11
|
+
|
|
12
|
+
#The result is a dataclass that allows easily accessing
|
|
13
|
+
# individual parts of the data and multiple ways of interpreting it
|
|
14
|
+
print("Result:")
|
|
15
|
+
#The base ThreespaceCmdResult dataclass
|
|
16
|
+
print(result)
|
|
17
|
+
#The actual byte response from the sensor that made this result (header + data)
|
|
18
|
+
print(result.raw_binary)
|
|
19
|
+
print()
|
|
20
|
+
print("Header:")
|
|
21
|
+
#The ThreespaceHeader data class
|
|
22
|
+
print(result.header)
|
|
23
|
+
#The individual components of the header as an array
|
|
24
|
+
#similar to how the old API functioned
|
|
25
|
+
print(result.header.raw)
|
|
26
|
+
#The actual byte representation of the header from the sensor
|
|
27
|
+
print(result.header.raw_binary)
|
|
28
|
+
print()
|
|
29
|
+
print("Data:")
|
|
30
|
+
print(f"{result.data=}")
|
|
31
|
+
print(f"{result.raw_data=}")
|
|
32
|
+
|
|
33
|
+
sensor.cleanup()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yostlabs
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2026.2.4
|
|
4
4
|
Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
|
|
5
5
|
Project-URL: Homepage, https://yostlabs.com/
|
|
6
6
|
Project-URL: Repository, https://github.com/YostLabs/3SpacePythonPackage/tree/main
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yostlabs"
|
|
7
7
|
#If uploading again on the same day, add a fourth number
|
|
8
|
-
version = "
|
|
8
|
+
version = "2026.2.4"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Yost Labs Inc.", email="techsupport@yostlabs.com" },
|
|
11
11
|
{ name="Andy Riedlinger", email="techsupport@yostlabs.com" },
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import yostlabs.math.vector as _vec
|
|
3
|
+
|
|
4
|
+
def quat_mul(a: list[float], b: list[float]):
|
|
5
|
+
out = [0, 0, 0, 0]
|
|
6
|
+
x = 0; y = 1; z = 2; w = 3
|
|
7
|
+
out[w] = a[w]*b[w] - a[x]*b[x] - a[y]*b[y] - a[z]*b[z]
|
|
8
|
+
out[x] = a[w]*b[x] + a[x]*b[w] + a[y]*b[z] - a[z]*b[y]
|
|
9
|
+
out[y] = a[w]*b[y] + a[y]*b[w] + a[z]*b[x] - a[x]*b[z]
|
|
10
|
+
out[z] = a[w]*b[z] + a[z]*b[w] + a[x]*b[y] - a[y]*b[x]
|
|
11
|
+
return out
|
|
12
|
+
|
|
13
|
+
#Rotates quaternion b by quaternion a, it does not combine them
|
|
14
|
+
def quat_rotate(a: list[float], b: list[float]):
|
|
15
|
+
inv = quat_inverse(a)
|
|
16
|
+
axis = [b[0], b[1], b[2], 0]
|
|
17
|
+
halfway = quat_mul(a, axis)
|
|
18
|
+
final = quat_mul(halfway, inv)
|
|
19
|
+
return [*final[:3], b[3]]
|
|
20
|
+
|
|
21
|
+
def quat_inverse(quat: list[float]):
|
|
22
|
+
#Note: While technically negating just the W is rotationally equivalent, this is not a good idea
|
|
23
|
+
#as it will conflict with rotating vectors, which are not rotations, by quaternions
|
|
24
|
+
return [-quat[0], -quat[1], -quat[2], quat[3]]
|
|
25
|
+
|
|
26
|
+
def quat_rotate_vec(quat: list[float], vec: list[float]):
|
|
27
|
+
inv = quat_inverse(quat)
|
|
28
|
+
tmp = [vec[0], vec[1], vec[2], 0]
|
|
29
|
+
halfway = quat_mul(quat, tmp)
|
|
30
|
+
final = quat_mul(halfway, inv)
|
|
31
|
+
return [final[0], final[1], final[2]]
|
|
32
|
+
|
|
33
|
+
def angles_to_quaternion(angles: list[float], order: str, degrees=True, extrinsic=False):
|
|
34
|
+
quat = [0, 0, 0, 1]
|
|
35
|
+
for i in range(len(angles)):
|
|
36
|
+
axis = order[i]
|
|
37
|
+
angle = angles[i]
|
|
38
|
+
if degrees:
|
|
39
|
+
angle = math.radians(angle)
|
|
40
|
+
unit_vec = _vec.axis_to_unit_vector(axis)
|
|
41
|
+
w = math.cos(angle / 2)
|
|
42
|
+
imaginary = math.sin(angle / 2)
|
|
43
|
+
unit_vec = [v * imaginary for v in unit_vec]
|
|
44
|
+
angle_quat = [*unit_vec, w]
|
|
45
|
+
if extrinsic:
|
|
46
|
+
quat = quat_mul(angle_quat, quat)
|
|
47
|
+
else:
|
|
48
|
+
quat = quat_mul(quat, angle_quat)
|
|
49
|
+
return quat
|
|
50
|
+
|
|
51
|
+
def quat_from_angles(angles: list[float], order: str, degrees=True, extrinsic=False):
|
|
52
|
+
return angles_to_quaternion(angles, order, degrees=degrees, extrinsic=extrinsic)
|
|
53
|
+
|
|
54
|
+
def quat_from_euler(angles: list[float], order: list[int], degrees=False, extrinsic=False):
|
|
55
|
+
return angles_to_quaternion(angles, order, degrees=degrees, extrinsic=extrinsic)
|
|
56
|
+
|
|
57
|
+
def quat_from_axis_angle(axis: list[float], angle: float):
|
|
58
|
+
imaginary = math.sin(angle / 2)
|
|
59
|
+
quat = [imaginary * v for v in axis]
|
|
60
|
+
quat.append(math.cos(angle / 2))
|
|
61
|
+
return quat
|
|
62
|
+
|
|
63
|
+
#There are multiple valid quats that can be returned by this. The intention of this function
|
|
64
|
+
#is to be able to rotate an arrow by the quat such that it points the correct direction. The rotation
|
|
65
|
+
#of that arrow along its axis may differ though
|
|
66
|
+
def quat_from_one_vector(vec: list[float]):
|
|
67
|
+
vec = _vec.vec_normalize(vec)
|
|
68
|
+
perpendicular = _vec.vec_normalize(_vec.vec_cross([0, 0, 1], vec))
|
|
69
|
+
angle = math.acos(_vec.vec_dot([0, 0, 1], vec))
|
|
70
|
+
return quat_from_axis_angle(perpendicular, angle)
|
|
71
|
+
|
|
72
|
+
def quat_from_two_vectors(forward: list[float], down: list[float]):
|
|
73
|
+
"""
|
|
74
|
+
This function requires two orthogonal vectors to work
|
|
75
|
+
"""
|
|
76
|
+
forward_reference = [0, 0, 1]
|
|
77
|
+
down_reference = [0, -1, 0]
|
|
78
|
+
|
|
79
|
+
forward = _vec.vec_normalize(forward)
|
|
80
|
+
down = _vec.vec_normalize(down)
|
|
81
|
+
|
|
82
|
+
#Create the first rotation to align the forward axis
|
|
83
|
+
axis_of_rotation = _vec.vec_cross(forward_reference, forward)
|
|
84
|
+
axis_of_rotation = _vec.vec_normalize(axis_of_rotation)
|
|
85
|
+
if not any(abs(v) > 0 for v in axis_of_rotation):
|
|
86
|
+
axis_of_rotation = down_reference #This is just a direct 180 degree rotation around any orthogonal axis, so just use the down_ref
|
|
87
|
+
dot = min(1, max(-1, _vec.vec_dot(forward_reference, forward)))
|
|
88
|
+
angle1 = math.acos(dot)
|
|
89
|
+
imaginary = math.sin(angle1/2)
|
|
90
|
+
quat = [v * imaginary for v in axis_of_rotation] #XYZ
|
|
91
|
+
quat.append(math.cos(angle1/2)) #W
|
|
92
|
+
|
|
93
|
+
#Update the reference to figure out where it is after the turn
|
|
94
|
+
down_reference = quat_rotate_vec(quat, down_reference)
|
|
95
|
+
|
|
96
|
+
#find the rotation to make the remaining reference align with its given value
|
|
97
|
+
axis_of_rotation = _vec.vec_cross(down_reference, down)
|
|
98
|
+
axis_of_rotation = _vec.vec_normalize(axis_of_rotation)
|
|
99
|
+
if not any(abs(v) > 0 for v in axis_of_rotation):
|
|
100
|
+
axis_of_rotation = forward #Rotate along the final forward vector until up is aligned
|
|
101
|
+
dot = min(1, max(-1, _vec.vec_dot(down_reference, down)))
|
|
102
|
+
angle2 = math.acos(dot)
|
|
103
|
+
imaginary = math.sin(angle2/2)
|
|
104
|
+
rotation = [v * imaginary for v in axis_of_rotation] #XYZ
|
|
105
|
+
rotation.append(math.cos(angle2/2)) #W
|
|
106
|
+
|
|
107
|
+
quat = quat_mul(rotation, quat)
|
|
108
|
+
return quat
|
|
109
|
+
|
|
110
|
+
def quaternion_to_3x3_rotation_matrix(quat):
|
|
111
|
+
"""
|
|
112
|
+
Convert a quaternion in form x, y, z, w to a rotation matrix
|
|
113
|
+
"""
|
|
114
|
+
x, y, z, w = quat
|
|
115
|
+
|
|
116
|
+
fTx = 2.0 * x
|
|
117
|
+
fTy = 2.0 * y
|
|
118
|
+
fTz = 2.0 * z
|
|
119
|
+
fTwx = fTx * w
|
|
120
|
+
fTwy = fTy * w
|
|
121
|
+
fTwz = fTz * w
|
|
122
|
+
fTxx = fTx * x
|
|
123
|
+
fTxy = fTy * x
|
|
124
|
+
fTxz = fTz * x
|
|
125
|
+
fTyy = fTy * y
|
|
126
|
+
fTyz = fTz * y
|
|
127
|
+
fTzz = fTz * z
|
|
128
|
+
|
|
129
|
+
out = [0] * 9
|
|
130
|
+
out[0] = 1.0-(fTyy+fTzz)
|
|
131
|
+
out[1] = fTxy-fTwz
|
|
132
|
+
out[2] = fTxz+fTwy
|
|
133
|
+
out[3] = fTxy+fTwz
|
|
134
|
+
out[4] = 1.0-(fTxx+fTzz)
|
|
135
|
+
out[5] = fTyz-fTwx
|
|
136
|
+
out[6] = fTxz-fTwy
|
|
137
|
+
out[7] = fTyz + fTwx
|
|
138
|
+
out[8] = 1.0 - (fTxx+fTyy)
|
|
139
|
+
|
|
140
|
+
return [
|
|
141
|
+
[out[0], out[1], out[2]],
|
|
142
|
+
[out[3], out[4], out[5]],
|
|
143
|
+
[out[6], out[7], out[8]],
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
#Quat is expected in XYZW order
|
|
147
|
+
#https://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/quat_2_euler_paper_ver2-1.pdf
|
|
148
|
+
def q2ea(in_quat: list[float], order: list[int]) -> list[float]:
|
|
149
|
+
X, Y, Z, W = 0, 1, 2, 3 #Index order helpers
|
|
150
|
+
if len(order) != 3: raise Exception()
|
|
151
|
+
if len(in_quat) != 4: raise Exception()
|
|
152
|
+
out = [0, 0, 0]
|
|
153
|
+
|
|
154
|
+
i1, i2, i3 = order
|
|
155
|
+
i1n = (i1+ 1) % 3
|
|
156
|
+
i1nn = (i1n + 1) % 3
|
|
157
|
+
|
|
158
|
+
#Find the direction the final axis ends up pointing after the full rotation
|
|
159
|
+
#Note that this vector does not change when the third rotation is being applied.
|
|
160
|
+
v3_rot = [0, 0, 0]
|
|
161
|
+
v3_rot[i3] = 1
|
|
162
|
+
v3_rot = quat_rotate_vec(in_quat, v3_rot)
|
|
163
|
+
|
|
164
|
+
#NOTE: Whenever using asin/acos, ensure the input is in range of -1 <= x <= 1
|
|
165
|
+
#All this math should result in that, but floating point sometimes causes values like 1.0000002 which can cause NANs
|
|
166
|
+
v3_rot = [max(-1, min(1, v)) for v in v3_rot]
|
|
167
|
+
|
|
168
|
+
#Can now discover the first 2 rotations
|
|
169
|
+
#This is because the first two rotations determine the direction of the final axis,
|
|
170
|
+
#and each rotation only affects 1 plane, therefore think of it like the first rotation
|
|
171
|
+
#positions the third axis underneath its final spot, and the second rotation swings it up to its final position.
|
|
172
|
+
#These can be calculated using trig and the known axis pattern.
|
|
173
|
+
|
|
174
|
+
#Non-Circular, Repeated Axes
|
|
175
|
+
#XZX, YXY, ZYZ
|
|
176
|
+
if (i1 == 0 and i2 == 2 and i3 == 0) or \
|
|
177
|
+
(i1 == 1 and i2 == 0 and i3 == 1) or \
|
|
178
|
+
(i1 == 2 and i2 == 1 and i3 == 2):
|
|
179
|
+
out[0] = math.atan2(v3_rot[i1nn], v3_rot[i1n])
|
|
180
|
+
out[1] = math.acos(v3_rot[i1])
|
|
181
|
+
#Non-Circular, Non-Repeated Axes
|
|
182
|
+
#XZY, YXZ, ZYX
|
|
183
|
+
elif (i1 == 0 and i2 == 2 and i3 == 1) or \
|
|
184
|
+
(i1 == 1 and i2 == 0 and i3 == 2) or \
|
|
185
|
+
(i1 == 2 and i2 == 1 and i3 == 0):
|
|
186
|
+
out[0] = math.atan2(v3_rot[i1nn], v3_rot[i1n])
|
|
187
|
+
out[1] = -math.asin(v3_rot[i1])
|
|
188
|
+
#Circular, Repeated Axes
|
|
189
|
+
#XYX, YZY, ZXZ
|
|
190
|
+
elif (i1 == 0 and i2 == 1 and i3 == 0) or \
|
|
191
|
+
(i1 == 1 and i2 == 2 and i3 == 1) or \
|
|
192
|
+
(i1 == 2 and i2 == 0 and i3 == 2):
|
|
193
|
+
out[0] = math.atan2(v3_rot[i1n], -v3_rot[i1nn])
|
|
194
|
+
out[1] = math.acos(v3_rot[i1])
|
|
195
|
+
#Circular, Non-Repeated Axes
|
|
196
|
+
#XYZ, YZX, ZXY
|
|
197
|
+
elif (i1 == 0 and i2 == 1 and i3 == 2) or \
|
|
198
|
+
(i1 == 1 and i2 == 2 and i3 == 0) or \
|
|
199
|
+
(i1 == 2 and i2 == 0 and i3 == 1):
|
|
200
|
+
out[0] = math.atan2(-v3_rot[i1n], v3_rot[i1nn])
|
|
201
|
+
out[1] = math.asin(v3_rot[i1]) #Note, the paper incorrectly says -asin
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError("Invalid Order")
|
|
204
|
+
|
|
205
|
+
#Now compute the third angle
|
|
206
|
+
|
|
207
|
+
#Create a quaternion that applies the first two rotations
|
|
208
|
+
q1_rotation = [0, 0, 0, 0]
|
|
209
|
+
q1_rotation[W] = math.cos(out[0] / 2)
|
|
210
|
+
q1_rotation[i1] = math.sin(out[0] / 2)
|
|
211
|
+
|
|
212
|
+
q2_rotation = [0, 0, 0, 0]
|
|
213
|
+
q2_rotation[W] = math.cos(out[1] / 2)
|
|
214
|
+
q2_rotation[i2] = math.sin(out[1] / 2)
|
|
215
|
+
|
|
216
|
+
q12 = quat_mul(q1_rotation, q2_rotation)
|
|
217
|
+
|
|
218
|
+
#Now apply that quaternion and the original quaternion to the axis that will be rotated by the last axis
|
|
219
|
+
#(Rotating along Axis 3 does not change the position of Axis 3, this is done to obtain an axis that will be rotated
|
|
220
|
+
#so that the difference can be computed to decide how much rotation is needed for the last angle)
|
|
221
|
+
i3n = (i3 + 1) % 3
|
|
222
|
+
v3n = [0, 0, 0]
|
|
223
|
+
v3n[i3n] = 1
|
|
224
|
+
|
|
225
|
+
v3_q12 = v3n.copy()
|
|
226
|
+
v3_q = v3n.copy()
|
|
227
|
+
v3_q12 = quat_rotate_vec(q12, v3_q12)
|
|
228
|
+
v3_q = quat_rotate_vec(in_quat, v3_q)
|
|
229
|
+
|
|
230
|
+
#Now do trig to figure out how much rotation, and what direction, is needed to rotate v3_q12 to v3_q
|
|
231
|
+
d = _vec.vec_dot(v3_q12, v3_q) #Get angle between the two, ensure in range of acos
|
|
232
|
+
d = max(-1, min(1, d))
|
|
233
|
+
magnitude = math.acos(d)
|
|
234
|
+
|
|
235
|
+
#Determine the sign of the rotation
|
|
236
|
+
cross = _vec.vec_cross(v3_q12, v3_q)
|
|
237
|
+
sign = _vec.vec_dot(cross, v3_rot)
|
|
238
|
+
if sign < 0: sign = -1
|
|
239
|
+
else: sign = 1
|
|
240
|
+
|
|
241
|
+
out[2] = sign * abs(magnitude)
|
|
242
|
+
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
def string_order_to_indices(order: str):
|
|
246
|
+
order = order.lower()
|
|
247
|
+
int_order = []
|
|
248
|
+
if len(order) != 3: raise ValueError()
|
|
249
|
+
for c in order:
|
|
250
|
+
if c == 'x': int_order.append(0)
|
|
251
|
+
elif c == 'y': int_order.append(1)
|
|
252
|
+
elif c == 'z': int_order.append(2)
|
|
253
|
+
else: raise ValueError()
|
|
254
|
+
|
|
255
|
+
return int_order
|
|
256
|
+
|
|
257
|
+
def quat_to_euler_angles(in_quat: list[float], order: str|list[int], extrinsic=False):
|
|
258
|
+
if isinstance(order, str):
|
|
259
|
+
if 'e' in order.lower():
|
|
260
|
+
extrinsic = True
|
|
261
|
+
elif 'i' in order.lower():
|
|
262
|
+
extrinsic = False
|
|
263
|
+
#Only care about the first 3 chars, the 4th optional char is extrinsic vs intrinsic
|
|
264
|
+
order = string_order_to_indices(order[:3])
|
|
265
|
+
|
|
266
|
+
#Extrinsic is performing the rotations around the axes of the original fixed system (Rotates around world axes)
|
|
267
|
+
#Intrinsic is performing the rotations around the axes of a coordinate system that rotates with each step (It rotates around the local axes of the rotating object).
|
|
268
|
+
#These two representations are very similar, and even related. An extrinsic rotation is the same as an intrinsic rotation by the same angles, but with the order inverted.
|
|
269
|
+
if extrinsic:
|
|
270
|
+
#Taking advantage of the above info that intrinsic = extrinsic but in reverse and vice versa.
|
|
271
|
+
order[0], order[2] = order[2], order[0]
|
|
272
|
+
|
|
273
|
+
#Actual conversion, everything else is just setup
|
|
274
|
+
angles = q2ea(in_quat, order)
|
|
275
|
+
|
|
276
|
+
if extrinsic:
|
|
277
|
+
#Swap back to the original desired order
|
|
278
|
+
angles[0], angles[2] = angles[2], angles[0]
|
|
279
|
+
|
|
280
|
+
return angles
|
|
281
|
+
|
|
282
|
+
def quat_from_euler_angles(angles: list[float], order: list[int], degrees=False, extrinsic=False):
|
|
283
|
+
if isinstance(order, str):
|
|
284
|
+
order = _vec.parse_axis_string(order)[0]
|
|
285
|
+
quat = [0, 0, 0, 1]
|
|
286
|
+
for i in range(len(angles)):
|
|
287
|
+
axis = order[i]
|
|
288
|
+
angle = angles[i]
|
|
289
|
+
if degrees:
|
|
290
|
+
angle = math.radians(angle)
|
|
291
|
+
|
|
292
|
+
#Create unit vector for this
|
|
293
|
+
unit_vec = [0, 0, 0]
|
|
294
|
+
unit_vec[axis] = 1
|
|
295
|
+
|
|
296
|
+
#Create quaternion for this rotation and apply to overall rotation
|
|
297
|
+
angle_quat = quat_from_axis_angle(unit_vec, angle)
|
|
298
|
+
if extrinsic:
|
|
299
|
+
quat = quat_mul(angle_quat, quat)
|
|
300
|
+
else:
|
|
301
|
+
quat = quat_mul(quat, angle_quat)
|
|
302
|
+
return quat
|
|
303
|
+
|
|
304
|
+
def quaternion_global_to_local(quat, vec):
|
|
305
|
+
inverse = quat_inverse(quat)
|
|
306
|
+
return quat_rotate_vec(inverse, vec)
|
|
307
|
+
|
|
308
|
+
def quaternion_local_to_global(quat, vec):
|
|
309
|
+
return quat_rotate_vec(quat, vec)
|
|
310
|
+
|
|
311
|
+
def quaternion_swap_axes(quat: list, old_order: str, new_order: str):
|
|
312
|
+
return quaternion_swap_axes_fast(quat, _vec.parse_axis_string_info(old_order), _vec.parse_axis_string_info(new_order))
|
|
313
|
+
|
|
314
|
+
def quaternion_swap_axes_fast(quat: list, old_parsed_order: list[list, list, bool], new_parsed_order: list[list, list, bool]):
|
|
315
|
+
"""
|
|
316
|
+
Like quaternion_swap_axes but uses the inputs of parsing the axis strings to avoid having to recompute
|
|
317
|
+
the storage types.
|
|
318
|
+
|
|
319
|
+
each order should be a sequence of [order, mults, right_handed]
|
|
320
|
+
"""
|
|
321
|
+
old_order, old_mults, old_right_handed = old_parsed_order
|
|
322
|
+
new_order, new_mults, new_right_handed = new_parsed_order
|
|
323
|
+
|
|
324
|
+
#Undo the old negations
|
|
325
|
+
base_quat = quat.copy()
|
|
326
|
+
for i, mult in enumerate(old_mults):
|
|
327
|
+
base_quat[i] *= mult
|
|
328
|
+
|
|
329
|
+
#Now swap the positions and apply new multipliers
|
|
330
|
+
new_quat = base_quat.copy()
|
|
331
|
+
for i in range(3):
|
|
332
|
+
new_quat[i] = base_quat[old_order.index(new_order[i])]
|
|
333
|
+
new_quat[i] *= new_mults[i]
|
|
334
|
+
|
|
335
|
+
if old_right_handed != new_right_handed:
|
|
336
|
+
#Different handed systems rotate opposite directions. So to maintain the same rotation,
|
|
337
|
+
#invert the quaternion
|
|
338
|
+
new_quat = quat_inverse(new_quat)
|
|
339
|
+
|
|
340
|
+
return new_quat
|
|
341
|
+
|
|
342
|
+
#https://splines.readthedocs.io/en/latest/rotation/slerp.html
|
|
343
|
+
def slerp(a, b, t):
|
|
344
|
+
dot = _vec.vec_dot(a, b)
|
|
345
|
+
if dot < 0: #To force it to be the shortest route
|
|
346
|
+
b = [-v for v in b]
|
|
347
|
+
|
|
348
|
+
theta = math.acos(dot)
|
|
349
|
+
sin_theta = math.sin(theta)
|
|
350
|
+
r1 = math.sin(1 - t) * theta / sin_theta
|
|
351
|
+
r2 = math.sin(t * theta) / sin_theta
|
|
352
|
+
a = [r1 * v for v in a]
|
|
353
|
+
b = [r2 * v for v in b]
|
|
354
|
+
return _vec.vec_normalize([v + w for v, w in zip(a, b)])
|
|
@@ -33,7 +33,8 @@ def vec_is_right_handed(order: str, negations: list[bool] = None):
|
|
|
33
33
|
return right_handed
|
|
34
34
|
|
|
35
35
|
def axis_to_unit_vector(axis: str):
|
|
36
|
-
axis
|
|
36
|
+
if isinstance(axis, str):
|
|
37
|
+
axis = axis.lower()
|
|
37
38
|
if axis == 'x' or axis == 0: return [1, 0, 0]
|
|
38
39
|
if axis == 'y' or axis == 1: return [0, 1, 0]
|
|
39
40
|
if axis == 'z' or axis == 2: return [0, 0, 1]
|
|
@@ -45,7 +46,7 @@ def parse_axis_string(axis: str):
|
|
|
45
46
|
axis = axis.lower()
|
|
46
47
|
order = [0, 1, 2]
|
|
47
48
|
multipliers = [1, 1, 1]
|
|
48
|
-
if 'x'
|
|
49
|
+
if any(c in axis for c in ['x', 'y', 'z']): #Using XYZ notation
|
|
49
50
|
index = 0
|
|
50
51
|
for c in axis:
|
|
51
52
|
if c == '-':
|
|
@@ -63,6 +63,9 @@ class ThreespaceCommand:
|
|
|
63
63
|
BINARY_START_BYTE = 0xf7
|
|
64
64
|
BINARY_START_BYTE_HEADER = 0xf9
|
|
65
65
|
|
|
66
|
+
BINARY_READ_SETTINGS_START_BYTE = 0xFA
|
|
67
|
+
BINARY_READ_SETTINGS_START_BYTE_HEADER = 0xFC
|
|
68
|
+
|
|
66
69
|
def __init__(self, name: str, num: int, in_format: str, out_format: str, custom_func: Callable = None):
|
|
67
70
|
self.info = ThreespaceCommandInfo(name, num, in_format, out_format)
|
|
68
71
|
self.in_format = _3space_format_to_external(self.info.in_format)
|
|
@@ -82,7 +85,7 @@ class ThreespaceCommand:
|
|
|
82
85
|
|
|
83
86
|
def send_command(self, com: ThreespaceOutputStream, *args, header_enabled = False):
|
|
84
87
|
cmd = self.format_cmd(*args, header_enabled=header_enabled)
|
|
85
|
-
com.write(cmd)
|
|
88
|
+
com.write(cmd)
|
|
86
89
|
|
|
87
90
|
#Read the command result from an already read buffer. This will modify the given buffer to remove
|
|
88
91
|
#that data as well
|
|
@@ -415,6 +418,7 @@ class StreamableCommands(Enum):
|
|
|
415
418
|
GetBatteryPercent = 202
|
|
416
419
|
GetBatteryStatus = 203
|
|
417
420
|
|
|
421
|
+
GetGpsActiveState = 214
|
|
418
422
|
GetGpsCoord = 215
|
|
419
423
|
GetGpsAltitude = 216
|
|
420
424
|
GetGpsFixState = 217
|
|
@@ -917,6 +921,40 @@ class ThreespaceSensor:
|
|
|
917
921
|
return list(response_dict.values())[0]
|
|
918
922
|
return response_dict
|
|
919
923
|
|
|
924
|
+
"""
|
|
925
|
+
WIP. Currently does not work with string values or have full validation.
|
|
926
|
+
"""
|
|
927
|
+
def get_setting_binary(self, key: str, format: str, use_threespace_format=True):
|
|
928
|
+
if use_threespace_format:
|
|
929
|
+
format = _3space_format_to_external(format)
|
|
930
|
+
checksum = sum(ord(v) for v in key) % 256
|
|
931
|
+
#Format = StartByte + Key + Null Terminator of key + Checksum
|
|
932
|
+
cmd = struct.pack(f"<B{len(key)}sBB", ThreespaceCommand.BINARY_READ_SETTINGS_START_BYTE_HEADER, key.encode(), 0, checksum)
|
|
933
|
+
self.com.write(cmd)
|
|
934
|
+
try:
|
|
935
|
+
min_response_len = key.index(';')
|
|
936
|
+
except ValueError:
|
|
937
|
+
min_response_len = len(key)
|
|
938
|
+
if min_response_len < len(THREESPACE_GET_SETTINGS_ERROR_RESPONSE):
|
|
939
|
+
min_response_len = len(THREESPACE_GET_SETTINGS_ERROR_RESPONSE)
|
|
940
|
+
min_response_len += (1 + THREESPACE_BINARY_SETTINGS_ID_SIZE) #Null terminator and header size
|
|
941
|
+
if self.__await_get_settings_binary(min_response_len) != THREESPACE_AWAIT_COMMAND_FOUND:
|
|
942
|
+
raise RuntimeError(f"Failed to get binary setting: {key}")
|
|
943
|
+
self.com.read(THREESPACE_BINARY_SETTINGS_ID_SIZE) #Read pass the Header ID
|
|
944
|
+
|
|
945
|
+
try:
|
|
946
|
+
echoed_key = self.com.read_until(b'\0')[:-1].decode()
|
|
947
|
+
if echoed_key != key.lower():
|
|
948
|
+
raise ValueError()
|
|
949
|
+
except:
|
|
950
|
+
raise ValueError()
|
|
951
|
+
result = struct.unpack(f"<{format}", self.com.read(struct.calcsize(format)))
|
|
952
|
+
self.com.read(2) #Remove the null terminator and the checksum
|
|
953
|
+
if len(result) == 1:
|
|
954
|
+
return result[0]
|
|
955
|
+
return result
|
|
956
|
+
|
|
957
|
+
|
|
920
958
|
#-----------Base Settings Parsing----------------
|
|
921
959
|
|
|
922
960
|
def __await_set_settings(self, timeout=2):
|
|
@@ -1004,6 +1042,31 @@ class ThreespaceSensor:
|
|
|
1004
1042
|
self.misaligned = False
|
|
1005
1043
|
return THREESPACE_AWAIT_COMMAND_FOUND
|
|
1006
1044
|
|
|
1045
|
+
"""
|
|
1046
|
+
Incomplete, binary settings protocol is Work In Progress
|
|
1047
|
+
"""
|
|
1048
|
+
def __await_get_settings_binary(self, min_resp_length: int, timeout=2, check_bootloader=False):
|
|
1049
|
+
start_time = time.time()
|
|
1050
|
+
|
|
1051
|
+
while True:
|
|
1052
|
+
remaining_time = timeout - (time.time() - start_time)
|
|
1053
|
+
if remaining_time <= 0:
|
|
1054
|
+
return THREESPACE_AWAIT_COMMAND_TIMEOUT
|
|
1055
|
+
|
|
1056
|
+
if self.com.length < min_resp_length: continue
|
|
1057
|
+
if check_bootloader and self.com.peek(2) == b'OK':
|
|
1058
|
+
return THREESPACE_AWAIT_BOOTLOADER
|
|
1059
|
+
|
|
1060
|
+
id = self.com.peek(THREESPACE_BINARY_SETTINGS_ID_SIZE)
|
|
1061
|
+
if struct.unpack("<L", id)[0] != THREESPACE_BINARY_READ_SETTINGS_ID:
|
|
1062
|
+
self.__internal_update(self.__try_peek_header())
|
|
1063
|
+
continue
|
|
1064
|
+
|
|
1065
|
+
#TODO: Check the rest of the message structure, not just the ID
|
|
1066
|
+
|
|
1067
|
+
self.misaligned = False
|
|
1068
|
+
return THREESPACE_AWAIT_COMMAND_FOUND
|
|
1069
|
+
|
|
1007
1070
|
#---------------------------------BASE COMMAND PARSING--------------------------------------
|
|
1008
1071
|
def __try_peek_header(self):
|
|
1009
1072
|
"""
|
|
@@ -1651,6 +1714,7 @@ class ThreespaceSensor:
|
|
|
1651
1714
|
def getBatteryVoltage(self) -> ThreespaceCmdResult[float]: ...
|
|
1652
1715
|
def getBatteryPercent(self) -> ThreespaceCmdResult[int]: ...
|
|
1653
1716
|
def getBatteryStatus(self) -> ThreespaceCmdResult[int]: ...
|
|
1717
|
+
def getGpsActiveState(self) -> ThreespaceCmdResult[bool]: ...
|
|
1654
1718
|
def getGpsCoord(self) -> ThreespaceCmdResult[list[float]]: ...
|
|
1655
1719
|
def getGpsAltitude(self) -> ThreespaceCmdResult[float]: ...
|
|
1656
1720
|
def getGpsFixState(self) -> ThreespaceCmdResult[int]: ...
|
|
@@ -1810,6 +1874,7 @@ _threespace_commands: list[ThreespaceCommand] = [
|
|
|
1810
1874
|
ThreespaceCommand("getBatteryPercent", 202, "", "b"),
|
|
1811
1875
|
ThreespaceCommand("getBatteryStatus", 203, "", "b"),
|
|
1812
1876
|
|
|
1877
|
+
ThreespaceCommand("getGpsActiveState", 214, "", "b"),
|
|
1813
1878
|
ThreespaceCommand("getGpsCoord", 215, "", "dd"),
|
|
1814
1879
|
ThreespaceCommand("getGpsAltitude", 216, "", "f"),
|
|
1815
1880
|
ThreespaceCommand("getGpsFixState", 217, "", "b"),
|
|
@@ -24,6 +24,8 @@ THREESPACE_OUTPUT_MODE_ASCII = 1
|
|
|
24
24
|
THREESPACE_OUTPUT_MODE_BINARY = 2
|
|
25
25
|
|
|
26
26
|
THREESPACE_GET_SETTINGS_ERROR_RESPONSE = "<KEY_ERROR>"
|
|
27
|
+
THREESPACE_BINARY_SETTINGS_ID_SIZE = 4
|
|
28
|
+
THREESPACE_BINARY_READ_SETTINGS_ID = 0xC695B5E1
|
|
27
29
|
|
|
28
30
|
#This is not comprehensive, just enough to seperate keys from debug messages
|
|
29
31
|
THREESPACE_SETTING_KEY_INVALID_CHARS = " :;"
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from yostlabs.tss3.api import ThreespaceSensor
|
|
2
|
-
from yostlabs.communication.serial import ThreespaceSerialComClass
|
|
3
|
-
|
|
4
|
-
#Create a sensor by auto detecting a ThreespaceSerialComClass
|
|
5
|
-
sensor = ThreespaceSensor(ThreespaceSerialComClass)
|
|
6
|
-
|
|
7
|
-
result = sensor.getPrimaryCorrectedAccelVec()
|
|
8
|
-
|
|
9
|
-
#Read out the result
|
|
10
|
-
accel_vec = result.data
|
|
11
|
-
|
|
12
|
-
#The result is a dataclass that allows easily accessing individual parts of the data and multiple ways of interpreting it
|
|
13
|
-
print("Result:")
|
|
14
|
-
print(result) #The base ThreespaceCmdResult dataclass
|
|
15
|
-
print(result.raw_binary) #The actual byte response from the sensor that made this result (header + data)
|
|
16
|
-
print()
|
|
17
|
-
print("Header:")
|
|
18
|
-
print(result.header) #The ThreespaceHeader data class
|
|
19
|
-
print(result.header.raw) #The individual components of the header as an array, similar to how the old API functioned
|
|
20
|
-
print(result.header.raw_binary) #The actual byte representation of the header from the sensor
|
|
21
|
-
print()
|
|
22
|
-
print("Data:")
|
|
23
|
-
print(f"{result.data=}")
|
|
24
|
-
print(f"{result.raw_data=}")
|
|
25
|
-
|
|
26
|
-
sensor.cleanup()
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import math
|
|
2
|
-
import yostlabs.math.vector as _vec
|
|
3
|
-
|
|
4
|
-
def quat_mul(a: list[float], b: list[float]):
|
|
5
|
-
out = [0, 0, 0, 0]
|
|
6
|
-
x = 0; y = 1; z = 2; w = 3
|
|
7
|
-
out[w] = a[w]*b[w] - a[x]*b[x] - a[y]*b[y] - a[z]*b[z]
|
|
8
|
-
out[x] = a[w]*b[x] + a[x]*b[w] + a[y]*b[z] - a[z]*b[y]
|
|
9
|
-
out[y] = a[w]*b[y] + a[y]*b[w] + a[z]*b[x] - a[x]*b[z]
|
|
10
|
-
out[z] = a[w]*b[z] + a[z]*b[w] + a[x]*b[y] - a[y]*b[x]
|
|
11
|
-
return out
|
|
12
|
-
|
|
13
|
-
#Rotates quaternion b by quaternion a, it does not combine them
|
|
14
|
-
def quat_rotate(a: list[float], b: list[float]):
|
|
15
|
-
inv = quat_inverse(a)
|
|
16
|
-
axis = [b[0], b[1], b[2], 0]
|
|
17
|
-
halfway = quat_mul(a, axis)
|
|
18
|
-
final = quat_mul(halfway, inv)
|
|
19
|
-
return [*final[:3], b[3]]
|
|
20
|
-
|
|
21
|
-
def quat_inverse(quat: list[float]):
|
|
22
|
-
#Note: While technically negating just the W is rotationally equivalent, this is not a good idea
|
|
23
|
-
#as it will conflict with rotating vectors, which are not rotations, by quaternions
|
|
24
|
-
return [-quat[0], -quat[1], -quat[2], quat[3]]
|
|
25
|
-
|
|
26
|
-
def quat_rotate_vec(quat: list[float], vec: list[float]):
|
|
27
|
-
inv = quat_inverse(quat)
|
|
28
|
-
tmp = [vec[0], vec[1], vec[2], 0]
|
|
29
|
-
halfway = quat_mul(quat, tmp)
|
|
30
|
-
final = quat_mul(halfway, inv)
|
|
31
|
-
return [final[0], final[1], final[2]]
|
|
32
|
-
|
|
33
|
-
def angles_to_quaternion(angles: list[float], order: str, degrees=True):
|
|
34
|
-
quat = [0, 0, 0, 1]
|
|
35
|
-
for i in range(len(angles)):
|
|
36
|
-
axis = order[i]
|
|
37
|
-
angle = angles[i]
|
|
38
|
-
if degrees:
|
|
39
|
-
angle = math.radians(angle)
|
|
40
|
-
unit_vec = _vec.axis_to_unit_vector(axis)
|
|
41
|
-
w = math.cos(angle / 2)
|
|
42
|
-
imaginary = math.sin(angle / 2)
|
|
43
|
-
unit_vec = [v * imaginary for v in unit_vec]
|
|
44
|
-
angle_quat = [*unit_vec, w]
|
|
45
|
-
quat = quat_mul(quat, angle_quat)
|
|
46
|
-
return quat
|
|
47
|
-
|
|
48
|
-
def quat_from_axis_angle(axis: list[float], angle: float):
|
|
49
|
-
imaginary = math.sin(angle / 2)
|
|
50
|
-
quat = [imaginary * v for v in axis]
|
|
51
|
-
quat.append(math.cos(angle / 2))
|
|
52
|
-
return quat
|
|
53
|
-
|
|
54
|
-
#There are multiple valid quats that can be returned by this. The intention of this function
|
|
55
|
-
#is to be able to rotate an arrow by the quat such that it points the correct direction. The rotation
|
|
56
|
-
#of that arrow along its axis may differ though
|
|
57
|
-
def quat_from_one_vector(vec: list[float]):
|
|
58
|
-
vec = _vec.vec_normalize(vec)
|
|
59
|
-
perpendicular = _vec.vec_normalize(_vec.vec_cross([0, 0, 1], vec))
|
|
60
|
-
angle = math.acos(_vec.vec_dot([0, 0, 1], vec))
|
|
61
|
-
return quat_from_axis_angle(perpendicular, angle)
|
|
62
|
-
|
|
63
|
-
def quat_from_two_vectors(forward: list[float], down: list[float]):
|
|
64
|
-
"""
|
|
65
|
-
This function requires two orthogonal vectors to work
|
|
66
|
-
"""
|
|
67
|
-
forward_reference = [0, 0, 1]
|
|
68
|
-
down_reference = [0, -1, 0]
|
|
69
|
-
|
|
70
|
-
forward = _vec.vec_normalize(forward)
|
|
71
|
-
down = _vec.vec_normalize(down)
|
|
72
|
-
|
|
73
|
-
#Create the first rotation to align the forward axis
|
|
74
|
-
axis_of_rotation = _vec.vec_cross(forward_reference, forward)
|
|
75
|
-
axis_of_rotation = _vec.vec_normalize(axis_of_rotation)
|
|
76
|
-
if not any(abs(v) > 0 for v in axis_of_rotation):
|
|
77
|
-
axis_of_rotation = down_reference #This is just a direct 180 degree rotation around any orthogonal axis, so just use the down_ref
|
|
78
|
-
dot = min(1, max(-1, _vec.vec_dot(forward_reference, forward)))
|
|
79
|
-
angle1 = math.acos(dot)
|
|
80
|
-
imaginary = math.sin(angle1/2)
|
|
81
|
-
quat = [v * imaginary for v in axis_of_rotation] #XYZ
|
|
82
|
-
quat.append(math.cos(angle1/2)) #W
|
|
83
|
-
|
|
84
|
-
#Update the reference to figure out where it is after the turn
|
|
85
|
-
down_reference = quat_rotate_vec(quat, down_reference)
|
|
86
|
-
|
|
87
|
-
#find the rotation to make the remaining reference align with its given value
|
|
88
|
-
axis_of_rotation = _vec.vec_cross(down_reference, down)
|
|
89
|
-
axis_of_rotation = _vec.vec_normalize(axis_of_rotation)
|
|
90
|
-
if not any(abs(v) > 0 for v in axis_of_rotation):
|
|
91
|
-
axis_of_rotation = forward #Rotate along the final forward vector until up is aligned
|
|
92
|
-
dot = min(1, max(-1, _vec.vec_dot(down_reference, down)))
|
|
93
|
-
angle2 = math.acos(dot)
|
|
94
|
-
imaginary = math.sin(angle2/2)
|
|
95
|
-
rotation = [v * imaginary for v in axis_of_rotation] #XYZ
|
|
96
|
-
rotation.append(math.cos(angle2/2)) #W
|
|
97
|
-
|
|
98
|
-
quat = quat_mul(rotation, quat)
|
|
99
|
-
return quat
|
|
100
|
-
|
|
101
|
-
def quaternion_to_3x3_rotation_matrix(quat):
|
|
102
|
-
"""
|
|
103
|
-
Convert a quaternion in form x, y, z, w to a rotation matrix
|
|
104
|
-
"""
|
|
105
|
-
x, y, z, w = quat
|
|
106
|
-
|
|
107
|
-
fTx = 2.0 * x
|
|
108
|
-
fTy = 2.0 * y
|
|
109
|
-
fTz = 2.0 * z
|
|
110
|
-
fTwx = fTx * w
|
|
111
|
-
fTwy = fTy * w
|
|
112
|
-
fTwz = fTz * w
|
|
113
|
-
fTxx = fTx * x
|
|
114
|
-
fTxy = fTy * x
|
|
115
|
-
fTxz = fTz * x
|
|
116
|
-
fTyy = fTy * y
|
|
117
|
-
fTyz = fTz * y
|
|
118
|
-
fTzz = fTz * z
|
|
119
|
-
|
|
120
|
-
out = [0] * 9
|
|
121
|
-
out[0] = 1.0-(fTyy+fTzz)
|
|
122
|
-
out[1] = fTxy-fTwz
|
|
123
|
-
out[2] = fTxz+fTwy
|
|
124
|
-
out[3] = fTxy+fTwz
|
|
125
|
-
out[4] = 1.0-(fTxx+fTzz)
|
|
126
|
-
out[5] = fTyz-fTwx
|
|
127
|
-
out[6] = fTxz-fTwy
|
|
128
|
-
out[7] = fTyz + fTwx
|
|
129
|
-
out[8] = 1.0 - (fTxx+fTyy)
|
|
130
|
-
|
|
131
|
-
return [
|
|
132
|
-
[out[0], out[1], out[2]],
|
|
133
|
-
[out[3], out[4], out[5]],
|
|
134
|
-
[out[6], out[7], out[8]],
|
|
135
|
-
]
|
|
136
|
-
|
|
137
|
-
def quaternion_global_to_local(quat, vec):
|
|
138
|
-
inverse = quat_inverse(quat)
|
|
139
|
-
return quat_rotate_vec(inverse, vec)
|
|
140
|
-
|
|
141
|
-
def quaternion_local_to_global(quat, vec):
|
|
142
|
-
return quat_rotate_vec(quat, vec)
|
|
143
|
-
|
|
144
|
-
def quaternion_swap_axes(quat: list, old_order: str, new_order: str):
|
|
145
|
-
return quaternion_swap_axes_fast(quat, _vec.parse_axis_string_info(old_order), _vec.parse_axis_string_info(new_order))
|
|
146
|
-
|
|
147
|
-
def quaternion_swap_axes_fast(quat: list, old_parsed_order: list[list, list, bool], new_parsed_order: list[list, list, bool]):
|
|
148
|
-
"""
|
|
149
|
-
Like quaternion_swap_axes but uses the inputs of parsing the axis strings to avoid having to recompute
|
|
150
|
-
the storage types.
|
|
151
|
-
|
|
152
|
-
each order should be a sequence of [order, mults, right_handed]
|
|
153
|
-
"""
|
|
154
|
-
old_order, old_mults, old_right_handed = old_parsed_order
|
|
155
|
-
new_order, new_mults, new_right_handed = new_parsed_order
|
|
156
|
-
|
|
157
|
-
#Undo the old negations
|
|
158
|
-
base_quat = quat.copy()
|
|
159
|
-
for i, mult in enumerate(old_mults):
|
|
160
|
-
base_quat[i] *= mult
|
|
161
|
-
|
|
162
|
-
#Now swap the positions and apply new multipliers
|
|
163
|
-
new_quat = base_quat.copy()
|
|
164
|
-
for i in range(3):
|
|
165
|
-
new_quat[i] = base_quat[old_order.index(new_order[i])]
|
|
166
|
-
new_quat[i] *= new_mults[i]
|
|
167
|
-
|
|
168
|
-
if old_right_handed != new_right_handed:
|
|
169
|
-
#Different handed systems rotate opposite directions. So to maintain the same rotation,
|
|
170
|
-
#invert the quaternion
|
|
171
|
-
new_quat = quat_inverse(new_quat)
|
|
172
|
-
|
|
173
|
-
return new_quat
|
|
174
|
-
|
|
175
|
-
#https://splines.readthedocs.io/en/latest/rotation/slerp.html
|
|
176
|
-
def slerp(a, b, t):
|
|
177
|
-
dot = _vec.vec_dot(a, b)
|
|
178
|
-
if dot < 0: #To force it to be the shortest route
|
|
179
|
-
b = [-v for v in b]
|
|
180
|
-
|
|
181
|
-
theta = math.acos(dot)
|
|
182
|
-
sin_theta = math.sin(theta)
|
|
183
|
-
r1 = math.sin(1 - t) * theta / sin_theta
|
|
184
|
-
r2 = math.sin(t * theta) / sin_theta
|
|
185
|
-
a = [r1 * v for v in a]
|
|
186
|
-
b = [r2 * v for v in b]
|
|
187
|
-
return _vec.vec_normalize([v + w for v, w in zip(a, b)])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|