@gkucmierz/utils 5.0.0 → 5.4.0

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.
@@ -0,0 +1,345 @@
1
+ /**
2
+ * @module developer-utils
3
+ */
4
+
5
+ import { is } from './type.mjs';
6
+
7
+ /**
8
+ * Default required agreeing datapoints ($K = 4$) in a standard $4\text{-of-}5$ quorum check.
9
+ * @type {number}
10
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQBEBRAMQEF0AZAFQIGkEBfBAMxijAQCIABAOYBrAK4BjMCACmMAF4B6EXBAAbAM48A3MGAYc+YuSp1G2oA|▶ Try it live in Instacode}
11
+ */
12
+ export const QUORUM_DEFAULT_K = 4;
13
+
14
+ /**
15
+ * Default total independent sources ($N = 5$) in a standard $4\text{-of-}5$ quorum check.
16
+ * @type {number}
17
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQBEBRAMQEF0AZAFQIDkEBfBAMxijAQCIABAOYBrAK4BjMCACmMAF4B6EXBAAbAM48A3MGAYc+YuSp1G2oA|▶ Try it live in Instacode}
18
+ */
19
+ export const QUORUM_DEFAULT_N = 5;
20
+
21
+ /**
22
+ * Default maximum relative error threshold ($0.01\% = 0.0001 = 1\text{ bp}$) for two datapoints
23
+ * to be considered equal in a quorum evaluation.
24
+ * @type {number}
25
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfTwEEANA7AUQBliAVASQDVKDLtscEBfBAMxhQwCAEQABAOYBrAK4BjMCACmMAF4B6GXBAAbAM4iA3MGAYc+ImQo16zVu07ZjQA|▶ Try it live in Instacode}
26
+ */
27
+ export const QUORUM_MAX_RELATIVE_ERROR = 0.0001;
28
+
29
+ /**
30
+ * Default maximum relative error expressed in percent ($0.01\%$).
31
+ * @type {number}
32
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfTwEEANA7AUQBliAVASQDVKDLtscCAFAYToQBfBADMYUMAgBEAAQDmAawCuAYzAgApjABeAeiVwQAGwDOUgNzBgGHPiJkKNes1btO2Hv0tA|▶ Try it live in Instacode}
33
+ */
34
+ export const QUORUM_MAX_RELATIVE_ERROR_PCT = 0.01;
35
+
36
+ /**
37
+ * Safe denominator floor near zero to avoid division by zero when comparing $0$ vs $0$.
38
+ * @type {number}
39
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQC0BRbTAgMQBlMcCSAFAZQElaA5BAXwQDMYUMAgBEAAQDmAawCuAYzAgApjABeAehlwQAGwDOIgNzBgGHPmJkKNOtgYt2mDsaA|▶ Try it live in Instacode}
40
+ */
41
+ export const QUORUM_ZERO_FLOOR_EPSILON = 1e-9;
42
+
43
+ /**
44
+ * Frozen SSoT configuration object for $K\text{-of-}N$ quorum evaluation.
45
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQGFMA5AMQEkBxBAXwQDMYowEAiAAQHMBrAVwDGYEAFMYALwD0-OCAA2AZ3YBuYMAw58xMlWpqgA|▶ Try it live in Instacode}
46
+ */
47
+ export const QUORUM_CONFIG = Object.freeze({
48
+ DEFAULT_K: QUORUM_DEFAULT_K,
49
+ DEFAULT_N: QUORUM_DEFAULT_N,
50
+ MAX_RELATIVE_ERROR: QUORUM_MAX_RELATIVE_ERROR,
51
+ MAX_RELATIVE_ERROR_PCT: QUORUM_MAX_RELATIVE_ERROR_PCT,
52
+ ZERO_FLOOR_EPSILON: QUORUM_ZERO_FLOOR_EPSILON
53
+ });
54
+
55
+ const isFiniteScalar = (v) => is(v, Number) && Number.isFinite(v);
56
+
57
+ const isNumericVector = (v) =>
58
+ is(v, Array) && v.length > 0 && v.every((item) => isFiniteScalar(item));
59
+
60
+ const cleanFloat = (val) => Number(Number(val).toFixed(10));
61
+
62
+ /**
63
+ * Computes the symmetric relative error between two numbers or two equal-length numeric vectors:
64
+ * $$\delta_{\text{rel}}(a, b) = \frac{|a - b|}{\max(|a|, |b|, \varepsilon_{\text{floor}})}$$
65
+ *
66
+ * For vectors $\mathbf{a}, \mathbf{b} \in \mathbb{R}^D$, returns the worst-case component-wise
67
+ * relative error $\max_{d} \delta_{\text{rel}}(a_d, b_d)$.
68
+ *
69
+ * @param {number|number[]} a - First value or numeric vector.
70
+ * @param {number|number[]} b - Second value or numeric vector.
71
+ * @param {Object} [options={}] - Options object.
72
+ * @param {number} [options.zeroFloor=1e-9] - Denominator floor when both values are near zero.
73
+ * @returns {number} Relative error in $[0, \infty)$ (`0.0001` corresponds to `0.01%`).
74
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMZUgVzgUwEo4A2AhnCAG44CiMMsCAvggGZ1gIBEAAgOYDWmVGBA4YALwD02EIQDOHANzBg6LLgIkylGnRhKgA|▶ Try it live in Instacode}
75
+ */
76
+ export const computeRelativeError = (
77
+ a,
78
+ b,
79
+ { zeroFloor = QUORUM_ZERO_FLOOR_EPSILON } = {}
80
+ ) => {
81
+ const safeFloor = isFiniteScalar(zeroFloor) && zeroFloor > 0 ? zeroFloor : QUORUM_ZERO_FLOOR_EPSILON;
82
+
83
+ if (isFiniteScalar(a) && isFiniteScalar(b)) {
84
+ if (a === b) return 0;
85
+ const denom = Math.max(Math.abs(a), Math.abs(b), safeFloor);
86
+ return Math.abs(a - b) / denom;
87
+ }
88
+
89
+ if (isNumericVector(a) && isNumericVector(b) && a.length === b.length) {
90
+ let maxErr = 0;
91
+ for (let i = 0; i < a.length; i++) {
92
+ const err = computeRelativeError(a[i], b[i], { zeroFloor: safeFloor });
93
+ if (err > maxErr) maxErr = err;
94
+ }
95
+ return maxErr;
96
+ }
97
+
98
+ return Infinity;
99
+ };
100
+
101
+ /**
102
+ * Checks whether two datapoints (`a` and `b`) are equal within the maximum relative error
103
+ * threshold (default `0.01%`, i.e. `maxRelativeError = 0.0001`).
104
+ *
105
+ * @param {number|number[]} a - First datapoint.
106
+ * @param {number|number[]} b - Second datapoint.
107
+ * @param {Object} [options={}] - Options object.
108
+ * @param {number} [options.maxRelativeError=0.0001] - Maximum allowed relative error (`0.0001` = `0.01%`).
109
+ * @param {number} [options.zeroFloor=1e-9] - Denominator floor near zero.
110
+ * @returns {boolean} True if and only if relative error $\le \text{maxRelativeError}$.
111
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4JAZwIoFdZbAUQEcsBDAGwQF8EAzGKMBAIgAEBzAaywGMwQBTGAC8A9FjggyaJgG5goTDhh4ipMnKA|▶ Try it live in Instacode}
112
+ */
113
+ export const isQuorumEqual = (
114
+ a,
115
+ b,
116
+ {
117
+ maxRelativeError = QUORUM_MAX_RELATIVE_ERROR,
118
+ zeroFloor = QUORUM_ZERO_FLOOR_EPSILON
119
+ } = {}
120
+ ) => {
121
+ const safeMax = isFiniteScalar(maxRelativeError) && maxRelativeError >= 0
122
+ ? maxRelativeError
123
+ : QUORUM_MAX_RELATIVE_ERROR;
124
+ return computeRelativeError(a, b, { zeroFloor }) <= safeMax + 1e-15;
125
+ };
126
+
127
+ const computeClusterMedianScalar = (values) => {
128
+ const sorted = [...values].sort((x, y) => x - y);
129
+ const mid = Math.floor(sorted.length / 2);
130
+ if (sorted.length % 2 === 1) {
131
+ return sorted[mid];
132
+ }
133
+ return cleanFloat((sorted[mid - 1] + sorted[mid]) / 2);
134
+ };
135
+
136
+ const computeClusterConsensusValue = (values) => {
137
+ if (values.length === 0) return null;
138
+ if (isFiniteScalar(values[0])) {
139
+ return computeClusterMedianScalar(values);
140
+ }
141
+ if (isNumericVector(values[0])) {
142
+ const dim = values[0].length;
143
+ const out = new Array(dim);
144
+ for (let d = 0; d < dim; d++) {
145
+ out[d] = computeClusterMedianScalar(values.map((v) => v[d]));
146
+ }
147
+ return out;
148
+ }
149
+ return null;
150
+ };
151
+
152
+ /**
153
+ * Evaluates a $K\text{-of-}N$ (default $4\text{-of-}5$) quorum across a set of datapoints.
154
+ *
155
+ * **Strict Equality-Clique Specification:**
156
+ * - Two datapoints $v_i, v_j$ are considered equal iff $\delta_{\text{rel}}(v_i, v_j) \le \text{maxRelativeError}$ (default `0.01%` = `0.0001`).
157
+ * - A subset of datapoints forms an agreeing cluster (clique) $C$ iff **every pair** $i, j \in C$
158
+ * is mutually equal within `maxRelativeError` ($\max_{i,j \in C} \delta_{\text{rel}}(v_i, v_j) \le \text{maxRelativeError}$).
159
+ * - `success: true` is returned **if and only if** the largest mutually-equal clique $C^*$ has size $|C^*| \ge K$
160
+ * (e.g., at least $4$ out of $5$ datapoints are mutually equal within $0.01\%$).
161
+ *
162
+ * @param {Array<number|number[]|{source?: string, id?: string, value: number|number[]}>} datapoints - Input datapoints.
163
+ * @param {Object} [options={}] - Quorum configuration options.
164
+ * @param {number} [options.k=4] - Minimum number of mutually-equal datapoints required for success ($K$).
165
+ * @param {number} [options.n=5] - Expected total number of independent sources ($N$).
166
+ * @param {number} [options.maxRelativeError=0.0001] - Maximum relative error tolerance (`0.0001` = `0.01%`).
167
+ * @param {number} [options.zeroFloor=1e-9] - Denominator floor near zero.
168
+ * @param {boolean} [options.strictN=false] - When `true`, requires `datapoints.length === n` in addition to $|C^*| \ge k$.
169
+ * @returns {Object} Quorum evaluation result with `success`, `consensusValue`, `maxRelativeError`, `agreeingSources`, and `outliers`.
170
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IKYDcCGAbArpuVARV1lzAQF8EAzGKCgIgAEBzAa1wGMwRUYAXgHpccENgDOjANzBgGHPkIkyYWUA|▶ Try it live in Instacode}
171
+ */
172
+ export const evaluateQuorum = (
173
+ datapoints = [],
174
+ {
175
+ k = QUORUM_DEFAULT_K,
176
+ n = QUORUM_DEFAULT_N,
177
+ maxRelativeError = QUORUM_MAX_RELATIVE_ERROR,
178
+ zeroFloor = QUORUM_ZERO_FLOOR_EPSILON,
179
+ strictN = false
180
+ } = {}
181
+ ) => {
182
+ const requiredK = Math.max(1, Math.floor(Number(k) || QUORUM_DEFAULT_K));
183
+ const expectedN = Math.max(requiredK, Math.floor(Number(n) || QUORUM_DEFAULT_N));
184
+ const safeMaxRelErr =
185
+ isFiniteScalar(maxRelativeError) && maxRelativeError >= 0
186
+ ? maxRelativeError
187
+ : QUORUM_MAX_RELATIVE_ERROR;
188
+
189
+ const list = is(datapoints, Array) ? datapoints : [];
190
+ const valid = [];
191
+
192
+ for (let idx = 0; idx < list.length; idx++) {
193
+ const item = list[idx];
194
+ const isEntryObj = is(item, Object) && !is(item, Array) && item !== null && 'value' in item;
195
+ const rawVal = isEntryObj ? item.value : item;
196
+ const source = isEntryObj && is(item.source || item.id, String)
197
+ ? String(item.source || item.id)
198
+ : `source-${idx + 1}`;
199
+
200
+ if (isFiniteScalar(rawVal) || isNumericVector(rawVal)) {
201
+ valid.push({
202
+ index: idx,
203
+ source,
204
+ value: isNumericVector(rawVal) ? [...rawVal] : rawVal
205
+ });
206
+ }
207
+ }
208
+
209
+ const m = valid.length;
210
+ const distMatrix = Array.from({ length: m }, () => new Array(m).fill(0));
211
+ const adjMatrix = Array.from({ length: m }, () => new Array(m).fill(false));
212
+
213
+ for (let i = 0; i < m; i++) {
214
+ adjMatrix[i][i] = true;
215
+ for (let j = i + 1; j < m; j++) {
216
+ const d = computeRelativeError(valid[i].value, valid[j].value, { zeroFloor });
217
+ distMatrix[i][j] = d;
218
+ distMatrix[j][i] = d;
219
+ const eq = d <= safeMaxRelErr + 1e-15;
220
+ adjMatrix[i][j] = eq;
221
+ adjMatrix[j][i] = eq;
222
+ }
223
+ }
224
+
225
+ // Exact maximum clique search (with min internal spread tie-breaker)
226
+ let bestClique = [];
227
+ let bestSpread = Infinity;
228
+
229
+ const computeCliqueSpread = (clique) => {
230
+ let maxD = 0;
231
+ for (let a = 0; a < clique.length; a++) {
232
+ for (let b = a + 1; b < clique.length; b++) {
233
+ const d = distMatrix[clique[a]][clique[b]];
234
+ if (d > maxD) maxD = d;
235
+ }
236
+ }
237
+ return maxD;
238
+ };
239
+
240
+ const searchClique = (startIdx, currentClique) => {
241
+ if (currentClique.length + (m - startIdx) < bestClique.length) {
242
+ return;
243
+ }
244
+
245
+ if (currentClique.length > 0) {
246
+ const spread = computeCliqueSpread(currentClique);
247
+ if (
248
+ currentClique.length > bestClique.length ||
249
+ (currentClique.length === bestClique.length && spread < bestSpread - 1e-15)
250
+ ) {
251
+ bestClique = [...currentClique];
252
+ bestSpread = spread;
253
+ }
254
+ }
255
+
256
+ for (let next = startIdx; next < m; next++) {
257
+ let compatible = true;
258
+ for (let c = 0; c < currentClique.length; c++) {
259
+ if (!adjMatrix[currentClique[c]][next]) {
260
+ compatible = false;
261
+ break;
262
+ }
263
+ }
264
+ if (compatible) {
265
+ currentClique.push(next);
266
+ searchClique(next + 1, currentClique);
267
+ currentClique.pop();
268
+ }
269
+ }
270
+ };
271
+
272
+ if (m > 0) {
273
+ searchClique(0, []);
274
+ } else {
275
+ bestSpread = 0;
276
+ }
277
+
278
+ const agreeingEntries = bestClique.map((idx) => valid[idx]);
279
+ const agreeingValues = agreeingEntries.map((e) => e.value);
280
+ const agreeingCount = agreeingEntries.length;
281
+ const nConditionMet = strictN ? list.length === expectedN && m === expectedN : true;
282
+ const success = Boolean(nConditionMet && agreeingCount >= requiredK);
283
+ const consensusValue = success ? computeClusterConsensusValue(agreeingValues) : null;
284
+ const referenceValue = consensusValue !== null
285
+ ? consensusValue
286
+ : computeClusterConsensusValue(agreeingValues);
287
+
288
+ const bestSet = new Set(bestClique);
289
+ const outliers = [];
290
+ for (let i = 0; i < m; i++) {
291
+ if (!bestSet.has(i)) {
292
+ const relErr = referenceValue !== null
293
+ ? computeRelativeError(valid[i].value, referenceValue, { zeroFloor })
294
+ : Infinity;
295
+ outliers.push({
296
+ index: valid[i].index,
297
+ source: valid[i].source,
298
+ value: valid[i].value,
299
+ relativeError: Number.isFinite(relErr) ? cleanFloat(relErr) : Infinity,
300
+ relativeErrorPct: Number.isFinite(relErr) ? Number((relErr * 100).toFixed(6)) : Infinity
301
+ });
302
+ }
303
+ }
304
+
305
+ const safeSpread = Number.isFinite(bestSpread) ? cleanFloat(bestSpread) : 0;
306
+
307
+ return {
308
+ success,
309
+ quorumReached: success,
310
+ status: success
311
+ ? agreeingCount >= expectedN
312
+ ? 'OK_UNANIMOUS'
313
+ : 'OK_QUORUM'
314
+ : 'REJECTED_NO_QUORUM',
315
+ consensusValue,
316
+ agreeingCount,
317
+ requiredK,
318
+ expectedN,
319
+ sampleCount: m,
320
+ maxRelativeError: safeSpread,
321
+ maxRelativeErrorPct: Number((safeSpread * 100).toFixed(6)),
322
+ thresholdRelativeError: safeMaxRelErr,
323
+ thresholdRelativeErrorPct: Number((safeMaxRelErr * 100).toFixed(4)),
324
+ agreeingIndices: agreeingEntries.map((e) => e.index),
325
+ agreeingSources: agreeingEntries.map((e) => e.source),
326
+ agreeingValues,
327
+ outliers
328
+ };
329
+ };
330
+
331
+ /**
332
+ * Convenience helper for the standard $4\text{-of-}5$ quorum check with $0.01\%$ (`0.0001`)
333
+ * maximum relative error tolerance.
334
+ *
335
+ * @param {Array<number|number[]|{source?: string, id?: string, value: number|number[]}>} datapoints - 5 source datapoints.
336
+ * @param {Object} [options={}] - Optional overrides (`maxRelativeError`, `strictN`, `zeroFloor`).
337
+ * @returns {Object} Quorum evaluation result.
338
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IKYDcCGAbArpuVARV1lzABYoAzAVgQF8FqYowEAiAAQHMBrXAGMwIVDABeAelxwQ2AM4cA3MGAYc+QiTKUatFUA|▶ Try it live in Instacode}
339
+ */
340
+ export const evaluateQuorum4of5 = (datapoints = [], options = {}) =>
341
+ evaluateQuorum(datapoints, {
342
+ ...options,
343
+ k: QUORUM_DEFAULT_K,
344
+ n: QUORUM_DEFAULT_N
345
+ });
@@ -21,6 +21,10 @@ export class Type {
21
21
  static OBJECT = 'object';
22
22
  static ARRAY = 'array';
23
23
  static FUNCTION = 'function';
24
+ static ASYNC_FUNCTION = 'asyncfunction';
25
+ static GENERATOR_FUNCTION = 'generatorfunction';
26
+ static ASYNC_GENERATOR_FUNCTION = 'asyncgeneratorfunction';
27
+ static CALLABLE = 'callable';
24
28
 
25
29
  // 3. Standard Built-ins
26
30
  static DATE = 'date';
@@ -56,6 +60,9 @@ export class Type {
56
60
  if (Array.isArray(expected)) {
57
61
  return expected.some(exp => Type.is(val, exp));
58
62
  }
63
+ if (expected === Type.CALLABLE || expected === 'callable') {
64
+ return Type.isCallable(val);
65
+ }
59
66
  if (typeof expected === 'function') {
60
67
  if (expected === Number) return typeof val === 'number' && !Number.isNaN(val);
61
68
  if (expected === Boolean) return typeof val === 'boolean' || val instanceof Boolean;
@@ -70,6 +77,16 @@ export class Type {
70
77
  return Type.of(val) === expected;
71
78
  }
72
79
 
80
+ /**
81
+ * Checks if a value is callable (regular function, async function, or generator).
82
+ * @param {*} val - Value to check.
83
+ * @returns {boolean} True if the value is any callable function.
84
+ */
85
+ static isCallable(val) {
86
+ const t = Type.of(val);
87
+ return t === 'function' || t === 'asyncfunction' || t === 'generatorfunction' || t === 'asyncgeneratorfunction';
88
+ }
89
+
73
90
  /**
74
91
  * Compares whether two values have the exact same canonical type.
75
92
  * Safely prevents JavaScript traps like typeof NaN === typeof 42 or typeof null === typeof {}.
@@ -109,3 +126,10 @@ export const of = Type.of;
109
126
  */
110
127
  export const same = Type.same;
111
128
 
129
+ /**
130
+ * Checks if a value is callable (regular function, async function, or generator).
131
+ * @see {@link Type.isCallable}
132
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4JAZwMIEMA2OsBGOApggL4IBmMUYCARAAIDmA1gK4DGYIxMAXgHp2cEDjT0A3MFCZc+IsWlA|▶ Try it live in Instacode}
133
+ */
134
+ export const isCallable = Type.isCallable;
135
+
@@ -70,3 +70,167 @@ export const getRotationMatrixFromVectors = (u, v) => {
70
70
  return axisAngleToMatrix4(axis, angle);
71
71
  };
72
72
 
73
+ export const normalizeQuat = (q) => {
74
+ const len = Math.hypot(q.w, q.x, q.y, q.z);
75
+ if (len === 0) return { w: 1, x: 0, y: 0, z: 0 };
76
+ return {
77
+ w: q.w / len,
78
+ x: q.x / len,
79
+ y: q.y / len,
80
+ z: q.z / len
81
+ };
82
+ };
83
+
84
+ export const axisAngleToQuat = (axis, angle) => {
85
+ const [x, y, z] = normalize(axis);
86
+ const half = angle * 0.5;
87
+ const s = Math.sin(half);
88
+ return normalizeQuat({
89
+ w: Math.cos(half),
90
+ x: x * s,
91
+ y: y * s,
92
+ z: z * s
93
+ });
94
+ };
95
+
96
+ export const multiplyQuat = (a, b) => ({
97
+ w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
98
+ x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
99
+ y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
100
+ z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w
101
+ });
102
+
103
+ export const rotateVecByQuat = (v, q) => {
104
+ const [vx, vy, vz] = v;
105
+ const { w, x, y, z } = q;
106
+ const tx = 2 * (y * vz - z * vy);
107
+ const ty = 2 * (z * vx - x * vz);
108
+ const tz = 2 * (x * vy - y * vx);
109
+ return [
110
+ vx + w * tx + (y * tz - z * ty),
111
+ vy + w * ty + (z * tx - x * tz),
112
+ vz + w * tz + (x * ty - y * tx)
113
+ ];
114
+ };
115
+
116
+ export const matToQuat = (m) => {
117
+ const trace = m[0] + m[5] + m[10];
118
+ let w, x, y, z;
119
+ if (trace > 0) {
120
+ const s = 0.5 / Math.sqrt(trace + 1);
121
+ w = 0.25 / s;
122
+ x = (m[6] - m[9]) * s;
123
+ y = (m[8] - m[2]) * s;
124
+ z = (m[1] - m[4]) * s;
125
+ } else if (m[0] > m[5] && m[0] > m[10]) {
126
+ const s = 2 * Math.sqrt(1 + m[0] - m[5] - m[10]);
127
+ w = (m[6] - m[9]) / s;
128
+ x = 0.25 * s;
129
+ y = (m[4] + m[1]) / s;
130
+ z = (m[8] + m[2]) / s;
131
+ } else if (m[5] > m[10]) {
132
+ const s = 2 * Math.sqrt(1 + m[5] - m[0] - m[10]);
133
+ w = (m[8] - m[2]) / s;
134
+ x = (m[4] + m[1]) / s;
135
+ y = 0.25 * s;
136
+ z = (m[6] + m[9]) / s;
137
+ } else {
138
+ const s = 2 * Math.sqrt(1 + m[10] - m[0] - m[5]);
139
+ w = (m[1] - m[4]) / s;
140
+ x = (m[8] + m[2]) / s;
141
+ y = (m[6] + m[9]) / s;
142
+ z = 0.25 * s;
143
+ }
144
+ return normalizeQuat({ w, x, y, z });
145
+ };
146
+
147
+ export const quatToMat = (q) => {
148
+ const { w, x, y, z } = normalizeQuat(q);
149
+ const xx = x * x, yy = y * y, zz = z * z;
150
+ const xy = x * y, xz = x * z, yz = y * z;
151
+ const wx = w * x, wy = w * y, wz = w * z;
152
+ return [
153
+ 1 - 2 * (yy + zz), 2 * (xy + wz), 2 * (xz - wy), 0,
154
+ 2 * (xy - wz), 1 - 2 * (xx + zz), 2 * (yz + wx), 0,
155
+ 2 * (xz + wy), 2 * (yz - wx), 1 - 2 * (xx + yy), 0,
156
+ 0, 0, 0, 1
157
+ ];
158
+ };
159
+
160
+ export const slerp = (q1, q2, t) => {
161
+ let dot = q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z;
162
+ let q2n = q2;
163
+ if (dot < 0) {
164
+ q2n = { w: -q2.w, x: -q2.x, y: -q2.y, z: -q2.z };
165
+ dot = -dot;
166
+ }
167
+ if (dot > 0.9995) {
168
+ return normalizeQuat({
169
+ w: q1.w + t * (q2n.w - q1.w),
170
+ x: q1.x + t * (q2n.x - q1.x),
171
+ y: q1.y + t * (q2n.y - q1.y),
172
+ z: q1.z + t * (q2n.z - q1.z)
173
+ });
174
+ }
175
+ const theta = Math.acos(Math.max(-1, Math.min(1, dot)));
176
+ const sinTheta = Math.sin(theta);
177
+ const a = Math.sin((1 - t) * theta) / sinTheta;
178
+ const b = Math.sin(t * theta) / sinTheta;
179
+ return {
180
+ w: a * q1.w + b * q2n.w,
181
+ x: a * q1.x + b * q2n.x,
182
+ y: a * q1.y + b * q2n.y,
183
+ z: a * q1.z + b * q2n.z
184
+ };
185
+ };
186
+
187
+ export const matToEulerYXZ = (m) => {
188
+ if (!m || m.length < 16) return { pitch: 0, yaw: 0, roll: 0 };
189
+ const m11 = m[0], m13 = m[8];
190
+ const m21 = m[1], m22 = m[5], m23 = m[9];
191
+ const m31 = m[2], m33 = m[10];
192
+
193
+ const clamp = (v) => Math.max(-1, Math.min(1, v));
194
+ const x = Math.asin(-clamp(m23));
195
+ let y, z;
196
+ if (Math.abs(m23) < 0.9999999) {
197
+ y = Math.atan2(m13, m33);
198
+ z = Math.atan2(m21, m22);
199
+ } else {
200
+ y = Math.atan2(-m31, m11);
201
+ z = 0;
202
+ }
203
+ const radToDeg = 180 / Math.PI;
204
+ return {
205
+ pitch: x * radToDeg,
206
+ yaw: y * radToDeg,
207
+ roll: z * radToDeg
208
+ };
209
+ };
210
+
211
+ export const eulerYXZToQuat = (pitchDeg, yawDeg, rollDeg) => {
212
+ const degToRad = Math.PI / 180;
213
+ const hx = (pitchDeg * degToRad) * 0.5;
214
+ const hy = (yawDeg * degToRad) * 0.5;
215
+ const hz = (rollDeg * degToRad) * 0.5;
216
+
217
+ const c1 = Math.cos(hx), s1 = Math.sin(hx);
218
+ const c2 = Math.cos(hy), s2 = Math.sin(hy);
219
+ const c3 = Math.cos(hz), s3 = Math.sin(hz);
220
+
221
+ return normalizeQuat({
222
+ w: c1 * c2 * c3 + s1 * s2 * s3,
223
+ x: s1 * c2 * c3 + c1 * s2 * s3,
224
+ y: c1 * s2 * c3 - s1 * c2 * s3,
225
+ z: c1 * c2 * s3 - s1 * s2 * c3
226
+ });
227
+ };
228
+
229
+ export const applyScreenDragQuat = (currentQuat, dx, dy, sensitivity = 0.007) => {
230
+ const qY = axisAngleToQuat([0, 1, 0], dx * sensitivity);
231
+ const qX = axisAngleToQuat([1, 0, 0], dy * sensitivity);
232
+ const qDelta = multiplyQuat(qX, qY);
233
+ return normalizeQuat(multiplyQuat(qDelta, currentQuat));
234
+ };
235
+
236
+