@forgeax/engine-math 0.1.2

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.
Files changed (150) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +294 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/_arbs.d.ts +36 -0
  5. package/dist/__tests__/_arbs.d.ts.map +1 -0
  6. package/dist/__tests__/_fixtures.d.ts +61 -0
  7. package/dist/__tests__/_fixtures.d.ts.map +1 -0
  8. package/dist/__tests__/bounds2.test.d.ts +2 -0
  9. package/dist/__tests__/bounds2.test.d.ts.map +1 -0
  10. package/dist/__tests__/box3.test.d.ts +2 -0
  11. package/dist/__tests__/box3.test.d.ts.map +1 -0
  12. package/dist/__tests__/easing.test.d.ts +2 -0
  13. package/dist/__tests__/easing.test.d.ts.map +1 -0
  14. package/dist/__tests__/euler.test-d.d.ts +2 -0
  15. package/dist/__tests__/euler.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/mat3.test-d.d.ts +2 -0
  17. package/dist/__tests__/mat3.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/mat4.property.test.d.ts +2 -0
  19. package/dist/__tests__/mat4.property.test.d.ts.map +1 -0
  20. package/dist/__tests__/mat4.test-d.d.ts +2 -0
  21. package/dist/__tests__/mat4.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/mat4.test.d.ts +2 -0
  23. package/dist/__tests__/mat4.test.d.ts.map +1 -0
  24. package/dist/__tests__/noise.test.d.ts +2 -0
  25. package/dist/__tests__/noise.test.d.ts.map +1 -0
  26. package/dist/__tests__/quat.basis.test.d.ts +2 -0
  27. package/dist/__tests__/quat.basis.test.d.ts.map +1 -0
  28. package/dist/__tests__/quat.interpolation.test.d.ts +2 -0
  29. package/dist/__tests__/quat.interpolation.test.d.ts.map +1 -0
  30. package/dist/__tests__/quat.lookat.test.d.ts +2 -0
  31. package/dist/__tests__/quat.lookat.test.d.ts.map +1 -0
  32. package/dist/__tests__/quat.property.test.d.ts +2 -0
  33. package/dist/__tests__/quat.property.test.d.ts.map +1 -0
  34. package/dist/__tests__/quat.rotateaxis.test.d.ts +2 -0
  35. package/dist/__tests__/quat.rotateaxis.test.d.ts.map +1 -0
  36. package/dist/__tests__/quat.test-d.d.ts +2 -0
  37. package/dist/__tests__/quat.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/ray.property.test.d.ts +2 -0
  39. package/dist/__tests__/ray.property.test.d.ts.map +1 -0
  40. package/dist/__tests__/ray.test.d.ts +2 -0
  41. package/dist/__tests__/ray.test.d.ts.map +1 -0
  42. package/dist/__tests__/types.test-d.d.ts +2 -0
  43. package/dist/__tests__/types.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/vec-catmull-rom.test.d.ts +2 -0
  45. package/dist/__tests__/vec-catmull-rom.test.d.ts.map +1 -0
  46. package/dist/__tests__/vec-smooth-damp.test.d.ts +2 -0
  47. package/dist/__tests__/vec-smooth-damp.test.d.ts.map +1 -0
  48. package/dist/__tests__/vec2.test-d.d.ts +2 -0
  49. package/dist/__tests__/vec2.test-d.d.ts.map +1 -0
  50. package/dist/__tests__/vec3.property.test.d.ts +2 -0
  51. package/dist/__tests__/vec3.property.test.d.ts.map +1 -0
  52. package/dist/__tests__/vec3.test-d.d.ts +2 -0
  53. package/dist/__tests__/vec3.test-d.d.ts.map +1 -0
  54. package/dist/__tests__/vec4.test-d.d.ts +2 -0
  55. package/dist/__tests__/vec4.test-d.d.ts.map +1 -0
  56. package/dist/_internal/epsilon.d.ts +9 -0
  57. package/dist/_internal/epsilon.d.ts.map +1 -0
  58. package/dist/_internal/scalar.d.ts +40 -0
  59. package/dist/_internal/scalar.d.ts.map +1 -0
  60. package/dist/box2.d.ts +35 -0
  61. package/dist/box2.d.ts.map +1 -0
  62. package/dist/box3.d.ts +76 -0
  63. package/dist/box3.d.ts.map +1 -0
  64. package/dist/circle2.d.ts +29 -0
  65. package/dist/circle2.d.ts.map +1 -0
  66. package/dist/color.d.ts +64 -0
  67. package/dist/color.d.ts.map +1 -0
  68. package/dist/easing.d.ts +18 -0
  69. package/dist/easing.d.ts.map +1 -0
  70. package/dist/euler.d.ts +51 -0
  71. package/dist/euler.d.ts.map +1 -0
  72. package/dist/f32-to-f16-bytes.d.ts +10 -0
  73. package/dist/f32-to-f16-bytes.d.ts.map +1 -0
  74. package/dist/frustum.d.ts +49 -0
  75. package/dist/frustum.d.ts.map +1 -0
  76. package/dist/index.d.ts +20 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.mjs +3688 -0
  79. package/dist/index.mjs.map +1 -0
  80. package/dist/mat3.d.ts +70 -0
  81. package/dist/mat3.d.ts.map +1 -0
  82. package/dist/mat4.d.ts +329 -0
  83. package/dist/mat4.d.ts.map +1 -0
  84. package/dist/noise.d.ts +12 -0
  85. package/dist/noise.d.ts.map +1 -0
  86. package/dist/quat.d.ts +312 -0
  87. package/dist/quat.d.ts.map +1 -0
  88. package/dist/ray.d.ts +144 -0
  89. package/dist/ray.d.ts.map +1 -0
  90. package/dist/ray2.d.ts +38 -0
  91. package/dist/ray2.d.ts.map +1 -0
  92. package/dist/sphere.d.ts +41 -0
  93. package/dist/sphere.d.ts.map +1 -0
  94. package/dist/types.d.ts +57 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/vec2.d.ts +76 -0
  97. package/dist/vec2.d.ts.map +1 -0
  98. package/dist/vec3.d.ts +82 -0
  99. package/dist/vec3.d.ts.map +1 -0
  100. package/dist/vec4.d.ts +61 -0
  101. package/dist/vec4.d.ts.map +1 -0
  102. package/package.json +57 -0
  103. package/src/__tests__/_arbs.ts +149 -0
  104. package/src/__tests__/_fixtures.ts +118 -0
  105. package/src/__tests__/bounds2.test.ts +146 -0
  106. package/src/__tests__/box3.test.ts +277 -0
  107. package/src/__tests__/easing.test.ts +109 -0
  108. package/src/__tests__/euler.test-d.ts +63 -0
  109. package/src/__tests__/mat3.test-d.ts +47 -0
  110. package/src/__tests__/mat4.property.test.ts +256 -0
  111. package/src/__tests__/mat4.test-d.ts +130 -0
  112. package/src/__tests__/mat4.test.ts +162 -0
  113. package/src/__tests__/noise.test.ts +75 -0
  114. package/src/__tests__/quat.basis.test.ts +134 -0
  115. package/src/__tests__/quat.interpolation.test.ts +45 -0
  116. package/src/__tests__/quat.lookat.test.ts +103 -0
  117. package/src/__tests__/quat.property.test.ts +150 -0
  118. package/src/__tests__/quat.rotateaxis.test.ts +149 -0
  119. package/src/__tests__/quat.test-d.ts +138 -0
  120. package/src/__tests__/ray.property.test.ts +105 -0
  121. package/src/__tests__/ray.test.ts +539 -0
  122. package/src/__tests__/types.test-d.ts +64 -0
  123. package/src/__tests__/vec-catmull-rom.test.ts +125 -0
  124. package/src/__tests__/vec-smooth-damp.test.ts +167 -0
  125. package/src/__tests__/vec2.test-d.ts +59 -0
  126. package/src/__tests__/vec3.property.test.ts +72 -0
  127. package/src/__tests__/vec3.test-d.ts +65 -0
  128. package/src/__tests__/vec4.test-d.ts +61 -0
  129. package/src/_internal/epsilon.ts +29 -0
  130. package/src/_internal/scalar.ts +90 -0
  131. package/src/box2.ts +168 -0
  132. package/src/box3.ts +372 -0
  133. package/src/circle2.ts +134 -0
  134. package/src/color.ts +177 -0
  135. package/src/easing.ts +49 -0
  136. package/src/euler.ts +239 -0
  137. package/src/f32-to-f16-bytes.ts +71 -0
  138. package/src/frustum.ts +240 -0
  139. package/src/index.ts +65 -0
  140. package/src/mat3.ts +286 -0
  141. package/src/mat4.ts +1334 -0
  142. package/src/noise.ts +78 -0
  143. package/src/quat.ts +847 -0
  144. package/src/ray.ts +575 -0
  145. package/src/ray2.ts +198 -0
  146. package/src/sphere.ts +138 -0
  147. package/src/types.ts +78 -0
  148. package/src/vec2.ts +229 -0
  149. package/src/vec3.ts +294 -0
  150. package/src/vec4.ts +235 -0
package/src/quat.ts ADDED
@@ -0,0 +1,847 @@
1
+ // quat.ts — quaternion namespace (M4 / T-027)
2
+ //
3
+ // 23-function surface (≥ 16 lower bound):
4
+ // create / clone / identity / fromAxisAngle / fromEuler / fromRotationMatrix /
5
+ // fromLookAt / fromUnitVectors / multiply / rotateAxis / slerp / nlerp / invert /
6
+ // conjugate / dot / length / lengthSq / transformVec3 / normalize / eulerY /
7
+ // right / up / forward
8
+ //
9
+ // Memory layout lock: Float32Array length 4 [x, y, z, w], **Hamilton convention**
10
+ // (graphics mainstream: glm / Three.js / wgpu-matrix / DirectXMath / glam are all Hamilton (x,y,z,w)).
11
+ // Identity = [0, 0, 0, 1].
12
+ //
13
+ // Degenerate convention (plan-strategy §appendix A degenerate registry #8-#13):
14
+ // - fromAxisAngle(0-axis, _) → identity (same convention as the M2 baseline)
15
+ // - fromEuler(x, y, z, 'unknown') → silent fallback to 'XYZ' (D-P2 + AC-06 no throw)
16
+ // - slerp(a, b, t) when dot(a,b) < -EPS_SLERP_DOT_LIMIT → negate b then slerp normally (D-P6)
17
+ // - slerp(a, b, t) when |dot| > 1 - EPS_SLERP_DOT_LIMIT → falls back to nlerp (avoids acos blow-up)
18
+ // - fromUnitVectors(v, -v) → pick perpendicular axis (prefer (0,1,0); fall back to (1,0,0) if
19
+ // collinear with v) and 180° rotation (D-P18)
20
+ // - fromUnitVectors(v, v) → identity
21
+ // - normalize(zero quat) → out = zero (same convention as vec.normalize)
22
+ //
23
+ // Four ironclad rules (gl-matrix wiki / research §F1):
24
+ // 1. Out-param first; 2. Aliasing-safe; 3. Module-as-namespace; 4. Float32Array by default.
25
+ //
26
+ // Related: requirements §Surface quat lower bound 16 + AC-06 no throw + AC-08 naming alignment +
27
+ // AC-01 brand + boundary-case quat row;
28
+ // plan-strategy §6 M4 + D-P2/D-P6/D-P18 + §appendix A degenerate registry #8-#13;
29
+ // research §Finding 3 glam Hamilton + §fact-correction 4 fromEuler unknown silenced;
30
+ // wiki/gl-matrix-overview §quat degenerate anchor + wiki/glam-rs-overview §Hamilton +
31
+ // wiki/wgpu-matrix-overview §quat namespace.
32
+ //
33
+ // Degenerate-semantics registry (plan-strategy.md §appendix A; shares numbering with mat;
34
+ // D-P16 dual promise: runtime silent + JSDoc `@degrade` + `@example` guard pattern landing together):
35
+ // #8 quat.fromAxisAngle(0-axis, _) → out = identity
36
+ // #9 quat.fromEuler(_, _, _, unknown) → silently computes as 'XYZ' (D-P2)
37
+ // #10 quat.slerp(a, b, t) dot < -1+ε → negate b then slerp (D-P6)
38
+ // #11 quat.slerp(a, b, t) |dot| > 1-ε → falls back to nlerp
39
+ // #12 quat.fromUnitVectors(v, -v) → perpendicular-axis 180° rotation (D-P18)
40
+ // #13 quat.fromUnitVectors(v, v) → out = identity
41
+
42
+ import { EPS_NORMALIZE, EPS_QUAT_PARALLEL, EPS_SLERP_DOT_LIMIT } from './_internal/epsilon';
43
+ import { lengthSq4, normalize4 } from './_internal/scalar';
44
+ import type { EulerOrder, Mat3Like, Quat, QuatLike, Vec3, Vec3Like } from './types';
45
+
46
+ export type { Quat, QuatLike };
47
+
48
+ // Canonical axes for the local-basis accessors (right / up / forward). −Z is the
49
+ // forward convention (RL-4), matching mat4.getForward and fromLookAt.
50
+ const UNIT_X = [1, 0, 0] as const;
51
+ const UNIT_Y = [0, 1, 0] as const;
52
+ const UNIT_NEG_Z = [0, 0, -1] as const;
53
+
54
+ /** Create a Quat (default all zero; callers usually call identity() right after). */
55
+ export function create(): Quat {
56
+ return new Float32Array(4) as Quat;
57
+ }
58
+
59
+ /** Allocate a new Quat copy. */
60
+ export function clone(a: QuatLike): Quat {
61
+ return Float32Array.of(a[0] as number, a[1] as number, a[2] as number, a[3] as number) as Quat;
62
+ }
63
+
64
+ /** out = identity quaternion [0, 0, 0, 1]. Returns out. */
65
+ export function identity(out: Quat): Quat {
66
+ out[0] = 0;
67
+ out[1] = 0;
68
+ out[2] = 0;
69
+ out[3] = 1;
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * out = quaternion representing rotation by angleRadians around axis. Returns out.
75
+ *
76
+ * axis does not have to be pre-normalized; this function normalizes it internally.
77
+ *
78
+ * @degrade axis is the zero vector (lengthSq < EPS_NORMALIZE) → out = identity
79
+ * (no throw, registry #8).
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * quat.fromAxisAngle(out, [0, 1, 0], Math.PI / 2);
84
+ * // Guard: if (vec3.lengthSq(axis) < EPS_NORMALIZE) skip;
85
+ * ```
86
+ */
87
+ export function fromAxisAngle(out: Quat, axis: Vec3Like, angleRadians: number): Quat {
88
+ const ax = axis[0] as number;
89
+ const ay = axis[1] as number;
90
+ const az = axis[2] as number;
91
+ const lenSq = ax * ax + ay * ay + az * az;
92
+ if (lenSq < EPS_NORMALIZE) {
93
+ return identity(out);
94
+ }
95
+ const inv = 1 / Math.sqrt(lenSq);
96
+ const half = angleRadians / 2;
97
+ const s = Math.sin(half);
98
+ out[0] = ax * inv * s;
99
+ out[1] = ay * inv * s;
100
+ out[2] = az * inv * s;
101
+ out[3] = Math.cos(half);
102
+ return out;
103
+ }
104
+
105
+ /**
106
+ * out = quaternion from intrinsic Euler angles (x, y, z, order). Returns out.
107
+ *
108
+ * Angle unit = **radians** (aligned with wgpu-matrix / Three.js; research §fact-correction 4
109
+ * deviates from gl-matrix's degrees).
110
+ * 6 orders: XYZ / YXZ / ZXY / ZYX / YZX / XZY (intrinsic rotation: literal order x → y → z).
111
+ * Implementation: split each axis rotation into single-axis quaternions, then Hamilton-multiply
112
+ * in `order`.
113
+ *
114
+ * @degrade order outside the EulerOrder union (only possible via `as any` cast) →
115
+ * **silently computes as if 'XYZ'** (D-P2 + AC-06 no throw; registry #9).
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * quat.fromEuler(out, 0.5, 0.3, 0.1, 'XYZ');
120
+ * quat.fromEuler(out, 0.5, 0.3, 0.1, 'unknown' as any); // silently computed as 'XYZ'
121
+ * // Guard: pass an EulerOrder union literal; the TS layer already blocks unknown strings.
122
+ * ```
123
+ */
124
+ export function fromEuler(out: Quat, x: number, y: number, z: number, order: EulerOrder): Quat {
125
+ // Single-axis half-angle quat coefficients
126
+ const c1 = Math.cos(x / 2);
127
+ const c2 = Math.cos(y / 2);
128
+ const c3 = Math.cos(z / 2);
129
+ const s1 = Math.sin(x / 2);
130
+ const s2 = Math.sin(y / 2);
131
+ const s3 = Math.sin(z / 2);
132
+
133
+ // Formulas from Three.js Quaternion.setFromEuler (intrinsic rotation, Hamilton convention).
134
+ // 6 orders dispatched via switch; unknown silently falls back to XYZ (D-P2).
135
+ switch (order) {
136
+ case 'XYZ':
137
+ out[0] = s1 * c2 * c3 + c1 * s2 * s3;
138
+ out[1] = c1 * s2 * c3 - s1 * c2 * s3;
139
+ out[2] = c1 * c2 * s3 + s1 * s2 * c3;
140
+ out[3] = c1 * c2 * c3 - s1 * s2 * s3;
141
+ break;
142
+ case 'YXZ':
143
+ out[0] = s1 * c2 * c3 + c1 * s2 * s3;
144
+ out[1] = c1 * s2 * c3 - s1 * c2 * s3;
145
+ out[2] = c1 * c2 * s3 - s1 * s2 * c3;
146
+ out[3] = c1 * c2 * c3 + s1 * s2 * s3;
147
+ break;
148
+ case 'ZXY':
149
+ out[0] = s1 * c2 * c3 - c1 * s2 * s3;
150
+ out[1] = c1 * s2 * c3 + s1 * c2 * s3;
151
+ out[2] = c1 * c2 * s3 + s1 * s2 * c3;
152
+ out[3] = c1 * c2 * c3 - s1 * s2 * s3;
153
+ break;
154
+ case 'ZYX':
155
+ out[0] = s1 * c2 * c3 - c1 * s2 * s3;
156
+ out[1] = c1 * s2 * c3 + s1 * c2 * s3;
157
+ out[2] = c1 * c2 * s3 - s1 * s2 * c3;
158
+ out[3] = c1 * c2 * c3 + s1 * s2 * s3;
159
+ break;
160
+ case 'YZX':
161
+ out[0] = s1 * c2 * c3 + c1 * s2 * s3;
162
+ out[1] = c1 * s2 * c3 + s1 * c2 * s3;
163
+ out[2] = c1 * c2 * s3 - s1 * s2 * c3;
164
+ out[3] = c1 * c2 * c3 - s1 * s2 * s3;
165
+ break;
166
+ case 'XZY':
167
+ out[0] = s1 * c2 * c3 - c1 * s2 * s3;
168
+ out[1] = c1 * s2 * c3 - s1 * c2 * s3;
169
+ out[2] = c1 * c2 * s3 + s1 * s2 * c3;
170
+ out[3] = c1 * c2 * c3 + s1 * s2 * s3;
171
+ break;
172
+ default:
173
+ // D-P2 + AC-06: unknown order silently falls back to 'XYZ' (no throw, no console.warn).
174
+ out[0] = s1 * c2 * c3 + c1 * s2 * s3;
175
+ out[1] = c1 * s2 * c3 - s1 * c2 * s3;
176
+ out[2] = c1 * c2 * s3 + s1 * s2 * c3;
177
+ out[3] = c1 * c2 * c3 - s1 * s2 * s3;
178
+ break;
179
+ }
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * out = quaternion from a 3x3 rotation matrix m (column-major, length 9). Returns out.
185
+ *
186
+ * Implementation: Shepperd's case-split (branch on trace / largest diagonal entry); numerically
187
+ * more stable than the direct trace method (avoids sqrt(0) when trace ≈ -1).
188
+ *
189
+ * Assumption: m is a pure rotation matrix (|det|=1, columns orthogonal). If not, the result is
190
+ * undefined but does not throw.
191
+ *
192
+ * @degrade m is not a pure rotation (contains shear / scale) → numerically undefined but does
193
+ * not throw (AC-06 no throw; callers should normalize column vectors first or avoid
194
+ * introducing non-pure-rotation matrices at the ECS-design layer).
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * quat.fromRotationMatrix(out, mat3InstanceColumnMajor);
199
+ * ```
200
+ */
201
+ export function fromRotationMatrix(out: Quat, m: Mat3Like): Quat {
202
+ const m00 = m[0] as number;
203
+ const m01 = m[1] as number;
204
+ const m02 = m[2] as number;
205
+ const m10 = m[3] as number;
206
+ const m11 = m[4] as number;
207
+ const m12 = m[5] as number;
208
+ const m20 = m[6] as number;
209
+ const m21 = m[7] as number;
210
+ const m22 = m[8] as number;
211
+
212
+ // m is column-major: m[col*3+row] → m00 is col=0,row=0; m01 is col=0,row=1.
213
+ // Rotation matrix R[row][col]:
214
+ // R[0][0]=m00, R[1][0]=m01, R[2][0]=m02
215
+ // R[0][1]=m10, R[1][1]=m11, R[2][1]=m12
216
+ // R[0][2]=m20, R[1][2]=m21, R[2][2]=m22
217
+ // Shepperd's formulas use R[i][j]; here the column-major indices are equivalent:
218
+ // trace = R[0][0]+R[1][1]+R[2][2] = m00+m11+m22
219
+ const trace = m00 + m11 + m22;
220
+ if (trace > 0) {
221
+ const s = 0.5 / Math.sqrt(trace + 1);
222
+ out[0] = (m12 - m21) * s; // (R[2][1] - R[1][2])
223
+ out[1] = (m20 - m02) * s; // (R[0][2] - R[2][0])
224
+ out[2] = (m01 - m10) * s; // (R[1][0] - R[0][1])
225
+ out[3] = 0.25 / s;
226
+ } else if (m00 > m11 && m00 > m22) {
227
+ const s = 2 * Math.sqrt(1 + m00 - m11 - m22);
228
+ out[0] = 0.25 * s;
229
+ out[1] = (m10 + m01) / s;
230
+ out[2] = (m20 + m02) / s;
231
+ out[3] = (m12 - m21) / s;
232
+ } else if (m11 > m22) {
233
+ const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
234
+ out[0] = (m10 + m01) / s;
235
+ out[1] = 0.25 * s;
236
+ out[2] = (m21 + m12) / s;
237
+ out[3] = (m20 - m02) / s;
238
+ } else {
239
+ const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
240
+ out[0] = (m20 + m02) / s;
241
+ out[1] = (m21 + m12) / s;
242
+ out[2] = 0.25 * s;
243
+ out[3] = (m01 - m10) / s;
244
+ }
245
+ return out;
246
+ }
247
+
248
+ /**
249
+ * out = orientation quaternion for an object placed at `eye` and facing `target`. Returns out.
250
+ *
251
+ * This is the ergonomic camera/look-at helper: it yields the WORLD-space orientation an entity's
252
+ * Transform.rotation needs so that its local -z axis points from `eye` toward `target` (the camera
253
+ * convention, matching `mat4.lookAt`). Use it instead of hand-wiring
254
+ * `mat4.lookAt → mat4.invert → mat3.fromMat4 → quat.fromRotationMatrix`; that chain is easy to get
255
+ * wrong (notably `fromRotationMatrix` takes a mat3, but `Mat4Like`≡`Mat3Like`≡`ArrayLike<number>`,
256
+ * so passing a mat4 typechecks and silently reads garbage → NaN → nothing renders).
257
+ *
258
+ * Convenience composition (like `mat4.computeViewProj`), not a primitive: it builds the same
259
+ * right/newUp/forward basis as `mat4.lookAt` and reuses `fromRotationMatrix` for the extraction.
260
+ *
261
+ * @degrade eye ≈ target (|eye-target| < EPS_NORMALIZE) → out = identity (same convention as
262
+ * `mat4.lookAt` degenerate #4; no throw, AC-06).
263
+ * @degrade up collinear with the view direction → alternative up auto-selected (same as
264
+ * `mat4.lookAt` #5).
265
+ *
266
+ * @example
267
+ * ```ts
268
+ * // aim a camera at the origin
269
+ * const q = quat.fromLookAt(quat.create(), [-2.5, 4.5, 9], [0, 0, 0], [0, 1, 0]);
270
+ * world.set(cameraEntity, Transform, { pos: [-2.5, 4.5, 9], rot: q });
271
+ * ```
272
+ */
273
+ export function fromLookAt(out: Quat, eye: Vec3Like, target: Vec3Like, up: Vec3Like): Quat {
274
+ const ex = eye[0] as number;
275
+ const ey = eye[1] as number;
276
+ const ez = eye[2] as number;
277
+
278
+ // forward = normalize(eye - target): the object's local -z points at the target, so +z = eye - target
279
+ // (right-handed camera convention, identical to mat4.lookAt).
280
+ let fx = ex - (target[0] as number);
281
+ let fy = ey - (target[1] as number);
282
+ let fz = ez - (target[2] as number);
283
+ const fLenSq = fx * fx + fy * fy + fz * fz;
284
+ if (fLenSq < EPS_NORMALIZE) {
285
+ return identity(out);
286
+ }
287
+ const fInv = 1 / Math.sqrt(fLenSq);
288
+ fx *= fInv;
289
+ fy *= fInv;
290
+ fz *= fInv;
291
+
292
+ const upx = up[0] as number;
293
+ const upy = up[1] as number;
294
+ const upz = up[2] as number;
295
+
296
+ // right = normalize(cross(up, forward)); degrade path mirrors mat4.lookAt (#5 alternative up).
297
+ let rx = upy * fz - upz * fy;
298
+ let ry = upz * fx - upx * fz;
299
+ let rz = upx * fy - upy * fx;
300
+ let rLenSq = rx * rx + ry * ry + rz * rz;
301
+ if (rLenSq < EPS_NORMALIZE) {
302
+ // up collinear with forward: pick alternative up = (0, 0, 1); if still collinear pick (0, 1, 0)
303
+ rx = -fy;
304
+ ry = fx;
305
+ rz = 0;
306
+ rLenSq = rx * rx + ry * ry + rz * rz;
307
+ if (rLenSq < EPS_NORMALIZE) {
308
+ rx = 0;
309
+ ry = -fz;
310
+ rz = fy;
311
+ rLenSq = rx * rx + ry * ry + rz * rz;
312
+ }
313
+ }
314
+ const rInv = 1 / Math.sqrt(rLenSq);
315
+ rx *= rInv;
316
+ ry *= rInv;
317
+ rz *= rInv;
318
+
319
+ // newUp = cross(forward, right)
320
+ const ux = fy * rz - fz * ry;
321
+ const uy = fz * rx - fx * rz;
322
+ const uz = fx * ry - fy * rx;
323
+
324
+ // World rotation columns are the basis vectors themselves (col0=right, col1=up, col2=forward) —
325
+ // this is the transpose of mat4.lookAt's view rotation, i.e. the camera's world orientation.
326
+ // Pack column-major into a length-9 mat3 for fromRotationMatrix (SSOT for the Shepperd extraction).
327
+ // Per-call alloc mirrors mat4.computeViewProj's convenience-composition grain (camera orientation
328
+ // is set rarely; no module-scoped mutable state to reason about).
329
+ const m3 = new Float32Array(9);
330
+ m3[0] = rx;
331
+ m3[1] = ry;
332
+ m3[2] = rz;
333
+ m3[3] = ux;
334
+ m3[4] = uy;
335
+ m3[5] = uz;
336
+ m3[6] = fx;
337
+ m3[7] = fy;
338
+ m3[8] = fz;
339
+ return fromRotationMatrix(out, m3);
340
+ }
341
+
342
+ /**
343
+ * out = quaternion that rotates the unit vector v to w (shortest arc). Returns out.
344
+ *
345
+ * Assumes v and w are normalized; if not, the caller is responsible.
346
+ *
347
+ * @degrade v ≈ w (dot > 1 - EPS_QUAT_PARALLEL) → out = identity (registry #13).
348
+ * @degrade v ≈ -w (dot < -1 + EPS_QUAT_PARALLEL) → pick a perpendicular axis and do a 180°
349
+ * rotation (prefer (0,1,0); fall back to (1,0,0) when collinear with v;
350
+ * registry #12 + D-P18).
351
+ *
352
+ * @example
353
+ * ```ts
354
+ * quat.fromUnitVectors(out, [1,0,0], [0,1,0]); // 90° around Z
355
+ * quat.fromUnitVectors(out, [1,0,0], [-1,0,0]); // 180° opposite: pick (0,1,0) axis
356
+ * ```
357
+ */
358
+ export function fromUnitVectors(out: Quat, v: Vec3Like, w: Vec3Like): Quat {
359
+ const vx = v[0] as number;
360
+ const vy = v[1] as number;
361
+ const vz = v[2] as number;
362
+ const wx = w[0] as number;
363
+ const wy = w[1] as number;
364
+ const wz = w[2] as number;
365
+
366
+ const d = vx * wx + vy * wy + vz * wz;
367
+
368
+ if (d > 1 - EPS_QUAT_PARALLEL) {
369
+ // same direction: identity
370
+ return identity(out);
371
+ }
372
+
373
+ if (d < -1 + EPS_QUAT_PARALLEL) {
374
+ // opposite direction: pick a perpendicular axis (D-P18).
375
+ // Prefer (0, 1, 0); when v is collinear with (0, 1, 0) (|vy| ~ 1) fall back to (1, 0, 0).
376
+ let ax: number;
377
+ let ay: number;
378
+ let az: number;
379
+ if (Math.abs(vy) < 1 - EPS_QUAT_PARALLEL) {
380
+ // axis = normalize(cross(v, (0, 1, 0)))
381
+ ax = -vz;
382
+ ay = 0;
383
+ az = vx;
384
+ } else {
385
+ // v is collinear with (0, 1, 0) → use (1, 0, 0)
386
+ // axis = normalize(cross(v, (1, 0, 0)))
387
+ ax = 0;
388
+ ay = vz;
389
+ az = -vy;
390
+ }
391
+ const axLen = Math.sqrt(ax * ax + ay * ay + az * az);
392
+ if (axLen < EPS_NORMALIZE) {
393
+ // extreme safety net (theoretically unreachable): write identity, no throw
394
+ return identity(out);
395
+ }
396
+ const axInv = 1 / axLen;
397
+ out[0] = ax * axInv;
398
+ out[1] = ay * axInv;
399
+ out[2] = az * axInv;
400
+ out[3] = 0; // cos(PI/2) = 0
401
+ return out;
402
+ }
403
+
404
+ // General case: half-vector method (numerically stable; consensus of wgpu-matrix / Three.js)
405
+ // q = (cross(v, w), 1 + dot(v, w)), then normalize
406
+ const cx = vy * wz - vz * wy;
407
+ const cy = vz * wx - vx * wz;
408
+ const cz = vx * wy - vy * wx;
409
+ const sw = 1 + d;
410
+ const len = Math.sqrt(cx * cx + cy * cy + cz * cz + sw * sw);
411
+ const inv = 1 / len;
412
+ out[0] = cx * inv;
413
+ out[1] = cy * inv;
414
+ out[2] = cz * inv;
415
+ out[3] = sw * inv;
416
+ return out;
417
+ }
418
+
419
+ /**
420
+ * out = a * b (Hamilton product). Returns out.
421
+ *
422
+ * Aliasing-safe: reads all 8 source elements into locals first.
423
+ */
424
+ export function multiply(out: Quat, a: QuatLike, b: QuatLike): Quat {
425
+ const ax = a[0] as number;
426
+ const ay = a[1] as number;
427
+ const az = a[2] as number;
428
+ const aw = a[3] as number;
429
+ const bx = b[0] as number;
430
+ const by = b[1] as number;
431
+ const bz = b[2] as number;
432
+ const bw = b[3] as number;
433
+ out[0] = aw * bx + ax * bw + ay * bz - az * by;
434
+ out[1] = aw * by - ax * bz + ay * bw + az * bx;
435
+ out[2] = aw * bz + ax * by - ay * bx + az * bw;
436
+ out[3] = aw * bw - ax * bx - ay * by - az * bz;
437
+ return out;
438
+ }
439
+
440
+ /**
441
+ * out = the orientation `q` after rotating a further `angleRadians` about world-space `axis`,
442
+ * re-normalized. Returns out.
443
+ *
444
+ * This is the ergonomic *incremental rotate* helper — the per-frame spin/animation move. It folds
445
+ * the three steps every rotating demo otherwise hand-wires: build the delta quaternion
446
+ * (`fromAxisAngle`), **pre**-multiply it onto the current orientation (world-space axis, matching
447
+ * **Bevy `Transform::rotate(r)` = `r * self.rotation`** / `rotate_y(θ)`), and — the step that is
448
+ * silently omitted and makes the naive loop wrong — **normalize** to shed the floating-point error
449
+ * that accumulates over thousands of frames into a non-unit quaternion (skew / scale artefacts).
450
+ *
451
+ * Prefer this over hand-writing `quat.multiply(q, delta, q)` in an update system: that loop drifts,
452
+ * so demos work around it with an absolute-angle accumulator + `fromAxisAngle` (can't compose onto an
453
+ * existing orientation) or raw `sin/cos` quaternion literals. `rotateAxis` composes safely.
454
+ *
455
+ * Convenience composition (like `fromLookAt`), not a primitive: same result as
456
+ * `normalize(out, multiply(out, fromAxisAngle(tmp, axis, angleRadians), q))`, fused + aliasing-safe.
457
+ *
458
+ * @degrade `axis` zero-length → the delta is identity (degenerate registry #8), so out = normalize(q)
459
+ * (no rotation applied; no throw, consistent with the sibling helpers).
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * // in an Update system: spin a cube about +Y at `speed` rad/s using the frame delta
464
+ * const dt = world.getResource(Time).delta;
465
+ * const t = world.get(entity, Transform).unwrap();
466
+ * quat.rotateAxis(t.quat, t.quat, [0, 1, 0], speed * dt); // in-place accumulate, no drift
467
+ * world.set(entity, Transform, t);
468
+ * ```
469
+ */
470
+ export function rotateAxis(out: Quat, q: QuatLike, axis: Vec3Like, angleRadians: number): Quat {
471
+ // Read q into locals first (aliasing-safe: rotateAxis(q, q, ...) is the common per-frame call).
472
+ const qx = q[0] as number;
473
+ const qy = q[1] as number;
474
+ const qz = q[2] as number;
475
+ const qw = q[3] as number;
476
+
477
+ // Delta quaternion for `angleRadians` about `axis` (fromAxisAngle inline; 0-axis → identity, #8).
478
+ const ax = axis[0] as number;
479
+ const ay = axis[1] as number;
480
+ const az = axis[2] as number;
481
+ const axisLen = Math.sqrt(ax * ax + ay * ay + az * az);
482
+ let dx = 0;
483
+ let dy = 0;
484
+ let dz = 0;
485
+ let dw = 1;
486
+ if (axisLen >= EPS_NORMALIZE) {
487
+ const half = angleRadians * 0.5;
488
+ const s = Math.sin(half) / axisLen;
489
+ dx = ax * s;
490
+ dy = ay * s;
491
+ dz = az * s;
492
+ dw = Math.cos(half);
493
+ }
494
+
495
+ // Pre-multiply: out = delta * q (world-space axis; matches Bevy Transform::rotate order).
496
+ let rx = dw * qx + dx * qw + dy * qz - dz * qy;
497
+ let ry = dw * qy - dx * qz + dy * qw + dz * qx;
498
+ let rz = dw * qz + dx * qy - dy * qx + dz * qw;
499
+ let rw = dw * qw - dx * qx - dy * qy - dz * qz;
500
+
501
+ // Normalize to kill accumulation drift (the whole point of this helper).
502
+ const len = Math.sqrt(rx * rx + ry * ry + rz * rz + rw * rw);
503
+ if (len >= EPS_NORMALIZE) {
504
+ const inv = 1 / len;
505
+ rx *= inv;
506
+ ry *= inv;
507
+ rz *= inv;
508
+ rw *= inv;
509
+ }
510
+ out[0] = rx;
511
+ out[1] = ry;
512
+ out[2] = rz;
513
+ out[3] = rw;
514
+ return out;
515
+ }
516
+
517
+ /**
518
+ * out = spherical linear interpolation(a, b, t). Returns out.
519
+ *
520
+ * t is not clamped (extrapolation semantics matches vec.lerp).
521
+ *
522
+ * @degrade dot(a, b) < -EPS_SLERP_DOT_LIMIT (anti-parallel) → **negate b' = -b then slerp**
523
+ * (D-P6; q and -q represent the same rotation; registry #10).
524
+ * @degrade |dot(a, b)| > 1 - EPS_SLERP_DOT_LIMIT (endpoint coincidence or near-coincidence) →
525
+ * falls back to nlerp (avoids acos blow-up; registry #11).
526
+ *
527
+ * @example
528
+ * ```ts
529
+ * quat.slerp(out, a, b, 0.5);
530
+ * // Anti-parallel (dot ≈ -1): caller does not need to pre-process; the negation fall-back
531
+ * // happens internally.
532
+ * ```
533
+ */
534
+ export function slerp(out: Quat, a: QuatLike, b: QuatLike, t: number): Quat {
535
+ const ax = a[0] as number;
536
+ const ay = a[1] as number;
537
+ const az = a[2] as number;
538
+ const aw = a[3] as number;
539
+ let bx = b[0] as number;
540
+ let by = b[1] as number;
541
+ let bz = b[2] as number;
542
+ let bw = b[3] as number;
543
+
544
+ let cosTheta = ax * bx + ay * by + az * bz + aw * bw;
545
+
546
+ // D-P6: anti-parallel → negate b then slerp (q and -q are the same rotation; shortest arc)
547
+ if (cosTheta < 0) {
548
+ bx = -bx;
549
+ by = -by;
550
+ bz = -bz;
551
+ bw = -bw;
552
+ cosTheta = -cosTheta;
553
+ }
554
+
555
+ // Endpoint coincidence (|dot| → 1): nlerp fall-back avoids sin(0) division by zero
556
+ if (cosTheta > 1 - EPS_SLERP_DOT_LIMIT) {
557
+ return nlerp(out, a, b, t);
558
+ }
559
+
560
+ const theta = Math.acos(cosTheta);
561
+ const sinTheta = Math.sin(theta);
562
+ const wa = Math.sin((1 - t) * theta) / sinTheta;
563
+ const wb = Math.sin(t * theta) / sinTheta;
564
+ out[0] = wa * ax + wb * bx;
565
+ out[1] = wa * ay + wb * by;
566
+ out[2] = wa * az + wb * bz;
567
+ out[3] = wa * aw + wb * bw;
568
+ return out;
569
+ }
570
+
571
+ /**
572
+ * out = normalized lerp(a, b, t). Returns out.
573
+ *
574
+ * Faster than slerp but the angular velocity is non-uniform; use it as a slerp substitute near endpoints.
575
+ * Like slerp, automatically handles dot < 0 by negating b to keep the shortest arc.
576
+ *
577
+ * @degrade lerp result is near zero (very rare) → out = identity.
578
+ *
579
+ * @example
580
+ * ```ts
581
+ * quat.nlerp(out, qA, qB, 0.5);
582
+ * // Very rare: lerp result lengthSq < EPS_NORMALIZE → out = (0, 0, 0, 1)
583
+ * ```
584
+ */
585
+ export function nlerp(out: Quat, a: QuatLike, b: QuatLike, t: number): Quat {
586
+ const ax = a[0] as number;
587
+ const ay = a[1] as number;
588
+ const az = a[2] as number;
589
+ const aw = a[3] as number;
590
+ let bx = b[0] as number;
591
+ let by = b[1] as number;
592
+ let bz = b[2] as number;
593
+ let bw = b[3] as number;
594
+
595
+ // anti-parallel → negate b for the shortest arc (same convention as slerp)
596
+ const cosTheta = ax * bx + ay * by + az * bz + aw * bw;
597
+ if (cosTheta < 0) {
598
+ bx = -bx;
599
+ by = -by;
600
+ bz = -bz;
601
+ bw = -bw;
602
+ }
603
+
604
+ out[0] = ax + t * (bx - ax);
605
+ out[1] = ay + t * (by - ay);
606
+ out[2] = az + t * (bz - az);
607
+ out[3] = aw + t * (bw - aw);
608
+
609
+ const lenSq =
610
+ (out[0] as number) * (out[0] as number) +
611
+ (out[1] as number) * (out[1] as number) +
612
+ (out[2] as number) * (out[2] as number) +
613
+ (out[3] as number) * (out[3] as number);
614
+ if (lenSq < EPS_NORMALIZE) {
615
+ return identity(out);
616
+ }
617
+ const inv = 1 / Math.sqrt(lenSq);
618
+ out[0] = (out[0] as number) * inv;
619
+ out[1] = (out[1] as number) * inv;
620
+ out[2] = (out[2] as number) * inv;
621
+ out[3] = (out[3] as number) * inv;
622
+ return out;
623
+ }
624
+
625
+ /**
626
+ * out = inverse of a (conjugate / lengthSq). Returns out.
627
+ *
628
+ * For unit quaternions, invert is equivalent to conjugate (and faster); this implementation uses
629
+ * the general a/|a|² form to support non-unit inputs.
630
+ *
631
+ * @degrade lengthSq(a) < EPS_NORMALIZE (zero quaternion) → out = identity (does not return NaN;
632
+ * same convention as mat invert).
633
+ *
634
+ * @example
635
+ * ```ts
636
+ * quat.invert(out, q);
637
+ * quat.invert(out, quat.create(0, 0, 0, 0)); // → out = identity (0, 0, 0, 1), AC-06 no throw
638
+ * ```
639
+ */
640
+ export function invert(out: Quat, a: QuatLike): Quat {
641
+ const ax = a[0] as number;
642
+ const ay = a[1] as number;
643
+ const az = a[2] as number;
644
+ const aw = a[3] as number;
645
+ const lenSq = ax * ax + ay * ay + az * az + aw * aw;
646
+ if (lenSq < EPS_NORMALIZE) {
647
+ return identity(out);
648
+ }
649
+ const inv = 1 / lenSq;
650
+ out[0] = -ax * inv;
651
+ out[1] = -ay * inv;
652
+ out[2] = -az * inv;
653
+ out[3] = aw * inv;
654
+ return out;
655
+ }
656
+
657
+ /** out = conjugate quaternion [-x, -y, -z, w] (aliasing-safe). Returns out. */
658
+ export function conjugate(out: Quat, a: QuatLike): Quat {
659
+ out[0] = -(a[0] as number);
660
+ out[1] = -(a[1] as number);
661
+ out[2] = -(a[2] as number);
662
+ out[3] = a[3] as number;
663
+ return out;
664
+ }
665
+
666
+ /** dot(a, b) = ax*bx + ay*by + az*bz + aw*bw (returns scalar). */
667
+ export function dot(a: QuatLike, b: QuatLike): number {
668
+ return (
669
+ (a[0] as number) * (b[0] as number) +
670
+ (a[1] as number) * (b[1] as number) +
671
+ (a[2] as number) * (b[2] as number) +
672
+ (a[3] as number) * (b[3] as number)
673
+ );
674
+ }
675
+
676
+ /** sqrt(x² + y² + z² + w²). Delegates to lengthSq + Math.sqrt (canonical pattern; see clone-5-quat-audit.md). */
677
+ export function length(a: QuatLike): number {
678
+ return Math.sqrt(lengthSq(a));
679
+ }
680
+
681
+ /** x² + y² + z² + w² (no sqrt). */
682
+ export function lengthSq(a: QuatLike): number {
683
+ return lengthSq4(a);
684
+ }
685
+
686
+ /**
687
+ * out = q · v · q⁻¹ (rotate vec3 v by the unit quaternion q). Returns out.
688
+ *
689
+ * Rodrigues optimized form (research Finding 3, 18 mul + 12 add):
690
+ * t = 2 · cross(q.xyz, v)
691
+ * out = v + q.w · t + cross(q.xyz, t)
692
+ *
693
+ * 3-5× faster than going through mat4 (fromQuat → transformVec3); same shape as gl-matrix
694
+ * `vec3.transformQuat` / Three.js `Vector3.applyQuaternion` (industry consensus).
695
+ *
696
+ * Aliasing-safe: reads v.xyz / q.xyzw into locals before writing out.
697
+ *
698
+ * @degrade q must be a unit-length quaternion. A non-unit q introduces implicit scaling
699
+ * (D-4 silent convention; no throw; AI users always get unit-length q from
700
+ * quat.fromAxisAngle / fromEuler / normalize / slerp, so this branch is not hit).
701
+ * When q = (0,0,0,0), t = 0 → out = v (the natural result of the formula; matches
702
+ * gl-matrix / Three.js; non-NaN, no throw).
703
+ *
704
+ * @example
705
+ * ```ts
706
+ * const q = quat.fromAxisAngle(quat.create(), [0, 1, 0], Math.PI / 2);
707
+ * const out = vec3.create();
708
+ * quat.transformVec3(out, q, [1, 0, 0]); // → out ≈ (0, 0, -1)
709
+ * // Guard: q derived from the quat surface is always unit-length; callers do not need to normalize.
710
+ * ```
711
+ */
712
+ export function transformVec3(out: Vec3, q: QuatLike, v: Vec3Like): Vec3 {
713
+ const qx = q[0] as number;
714
+ const qy = q[1] as number;
715
+ const qz = q[2] as number;
716
+ const qw = q[3] as number;
717
+ const vx = v[0] as number;
718
+ const vy = v[1] as number;
719
+ const vz = v[2] as number;
720
+ // t = 2 * cross(q.xyz, v)
721
+ const tx = 2 * (qy * vz - qz * vy);
722
+ const ty = 2 * (qz * vx - qx * vz);
723
+ const tz = 2 * (qx * vy - qy * vx);
724
+ // out = v + q.w * t + cross(q.xyz, t)
725
+ out[0] = vx + qw * tx + (qy * tz - qz * ty);
726
+ out[1] = vy + qw * ty + (qz * tx - qx * tz);
727
+ out[2] = vz + qw * tz + (qx * ty - qy * tx);
728
+ return out;
729
+ }
730
+
731
+ /**
732
+ * out = a / length(a). Returns out.
733
+ *
734
+ * @degrade lengthSq(a) < EPS_NORMALIZE → out = [0, 0, 0, 0] (same convention as vec.normalize;
735
+ * does not write identity, preserving the "zero quaternion" semantics; if the caller
736
+ * needs identity it should use fromAxisAngle with 0-axis).
737
+ *
738
+ * @example
739
+ * ```ts
740
+ * quat.normalize(out, q);
741
+ * quat.normalize(out, quat.create(0, 0, 0, 0)); // → out = (0, 0, 0, 0); zero-quaternion semantics preserved
742
+ * ```
743
+ */
744
+ export function normalize(out: Quat, a: QuatLike): Quat {
745
+ normalize4(out, a, EPS_NORMALIZE);
746
+ return out;
747
+ }
748
+
749
+ /**
750
+ * Convenience: quaternion representing rotation `theta` radians around the
751
+ * Y axis. Equivalent to `quat.fromEuler(out, 0, theta, 0, 'YXZ')` but
752
+ * allocates its own output `Quat`.
753
+ *
754
+ * Formula: `(0, sin(theta/2), 0, cos(theta/2))`.
755
+ *
756
+ * @degrade eulerY(0) → identity, eulerY(2pi) → identity within epsilon.
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * const yaw45 = quat.eulerY(Math.PI / 4);
761
+ * // yaw45 ≈ [0, 0.3826834, 0, 0.9238795]
762
+ * ```
763
+ *
764
+ * @example Equivalent to the verbose form:
765
+ * ```ts
766
+ * const q1 = quat.eulerY(theta);
767
+ * const q2 = quat.fromEuler(quat.create(), 0, theta, 0, 'YXZ');
768
+ * // q1 === q2 (within epsilon)
769
+ * ```
770
+ */
771
+ export function eulerY(theta: number): Quat {
772
+ const out = create();
773
+ return fromEuler(out, 0, theta, 0, 'YXZ');
774
+ }
775
+
776
+ // ── Local basis accessors ─────────────────────────────────────────────────
777
+ //
778
+ // A rotation's three local basis vectors — the world-space directions its own
779
+ // +X / +Y / −Z axes point after the rotation. They are exactly `q` applied to
780
+ // the canonical axes: right = q·(1,0,0), up = q·(0,1,0), forward = q·(0,0,−1).
781
+ //
782
+ // The −Z forward convention matches `mat4.getForward` (RL-4) and the look
783
+ // convention `fromLookAt` / `computeViewProj` use, so a camera/listener built
784
+ // with `fromLookAt(eye, target)` has `forward(q)` ≈ normalize(target − eye).
785
+ // These fold the transformVec3-with-a-magic-axis idiom (and the handedness a
786
+ // caller would otherwise have to know) into a named accessor, mirroring the
787
+ // mat4 getters so learning one basis form applies to both. Because `q` from the
788
+ // quat surface is always unit-length, `transformVec3` returns a unit vector — no
789
+ // separate normalize step (unlike the mat4 getters, which read possibly-scaled
790
+ // basis columns and must normalize).
791
+
792
+ /**
793
+ * Local right axis: the world-space direction the rotation's own +X axis points.
794
+ * `right(out, q) = quat.transformVec3(out, q, [1, 0, 0])`. Mirrors
795
+ * `mat4.getRight`; matches Bevy `Transform::local_x` / `Transform::right`.
796
+ *
797
+ * @degrade q must be unit-length (guaranteed by the quat surface); a unit q
798
+ * yields a unit result. q = (0,0,0,0) → out = (1,0,0) (the natural
799
+ * transformVec3 result; non-NaN, no throw).
800
+ *
801
+ * @example
802
+ * ```ts
803
+ * const q = quat.eulerY(Math.PI / 2); // 90° about +Y
804
+ * quat.right(vec3.create(), q); // → (0, 0, -1): +X yawed a quarter-turn
805
+ * ```
806
+ */
807
+ export function right(out: Vec3, q: QuatLike): Vec3 {
808
+ return transformVec3(out, q, UNIT_X);
809
+ }
810
+
811
+ /**
812
+ * Local up axis: the world-space direction the rotation's own +Y axis points.
813
+ * `up(out, q) = quat.transformVec3(out, q, [0, 1, 0])`. Mirrors `mat4.getUp`;
814
+ * matches Bevy `Transform::local_y` / `Transform::up`.
815
+ *
816
+ * @degrade q must be unit-length (guaranteed by the quat surface); a unit q
817
+ * yields a unit result. q = (0,0,0,0) → out = (0,1,0).
818
+ *
819
+ * @example
820
+ * ```ts
821
+ * const q = quat.fromAxisAngle(quat.create(), [1, 0, 0], Math.PI / 2); // pitch 90°
822
+ * quat.up(vec3.create(), q); // → (0, 0, 1): +Y pitched onto +Z
823
+ * ```
824
+ */
825
+ export function up(out: Vec3, q: QuatLike): Vec3 {
826
+ return transformVec3(out, q, UNIT_Y);
827
+ }
828
+
829
+ /**
830
+ * Local forward axis: the world-space direction the rotation's own −Z axis
831
+ * points (−Z look convention, RL-4). `forward(out, q) =
832
+ * quat.transformVec3(out, q, [0, 0, -1])`. Mirrors `mat4.getForward`; matches
833
+ * Bevy `Transform::forward` (Bevy also uses −Z). A quat from `fromLookAt(eye,
834
+ * target, up)` has `forward(q)` ≈ normalize(target − eye).
835
+ *
836
+ * @degrade q must be unit-length (guaranteed by the quat surface); a unit q
837
+ * yields a unit result. q = (0,0,0,0) → out = (0,0,-1).
838
+ *
839
+ * @example
840
+ * ```ts
841
+ * const q = quat.eulerY(Math.PI / 2); // 90° about +Y
842
+ * quat.forward(vec3.create(), q); // → (-1, 0, 0): -Z yawed a quarter-turn
843
+ * ```
844
+ */
845
+ export function forward(out: Vec3, q: QuatLike): Vec3 {
846
+ return transformVec3(out, q, UNIT_NEG_Z);
847
+ }