@series-inc/rundot-syncplay 5.26.0-beta.2 → 5.26.0-beta.3

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,741 @@
1
+ #include "fixed_math.hpp"
2
+
3
+ #include <cmath>
4
+ #include <limits>
5
+
6
+ namespace rundot::fixedmath {
7
+ namespace {
8
+
9
+ // ECMAScript Math.round, which is NOT std::round: it breaks halves toward +inf
10
+ // (Math.round(-2.5) === -2) where std::round breaks them away from zero (-3).
11
+ // toFixed is the only entry point where a real argument reaches this, and it is
12
+ // exactly where a std::round port would silently diverge.
13
+ double jsRound(double x) {
14
+ if (std::isnan(x) || std::isinf(x) || x == 0.0) return x;
15
+ if (x > 0.0 && x < 0.5) return 0.0;
16
+ if (x < 0.0 && x >= -0.5) return -0.0;
17
+ // At and above 2^52 the spacing is >= 1, so every double is already an
18
+ // integer and Math.round is identity. floor(x + 0.5) is NOT: for an odd x the
19
+ // sum ties and rounds to even, yielding x + 1.
20
+ if (std::fabs(x) >= 4503599627370496.0) return x;
21
+ return std::floor(x + 0.5);
22
+ }
23
+
24
+ // Number.isInteger: finite and integral. Non-integers make the reference throw.
25
+ bool isFixed(double v) { return std::isfinite(v) && std::floor(v) == v; }
26
+
27
+ bool isSafeInteger(double v) { return isFixed(v) && std::fabs(v) <= 9007199254740991.0; }
28
+
29
+ // NOT std::fmin/std::fmax: those IGNORE a NaN operand and return the other one,
30
+ // where Math.min/Math.max propagate it. They also disagree on signed zero, which
31
+ // Math.min/Math.max order (-0 < +0). clamp is the one operation with no integer
32
+ // assertion, so a real or NaN argument reaches these from creator code.
33
+ double jsMax(double a, double b) {
34
+ if (std::isnan(a) || std::isnan(b)) return std::numeric_limits<double>::quiet_NaN();
35
+ if (a == 0.0 && b == 0.0) return (std::signbit(a) && std::signbit(b)) ? -0.0 : 0.0;
36
+ return a > b ? a : b;
37
+ }
38
+
39
+ double jsMin(double a, double b) {
40
+ if (std::isnan(a) || std::isnan(b)) return std::numeric_limits<double>::quiet_NaN();
41
+ if (a == 0.0 && b == 0.0) return (std::signbit(a) || std::signbit(b)) ? -0.0 : 0.0;
42
+ return a < b ? a : b;
43
+ }
44
+
45
+ double saturate(double v, double lo, double hi) {
46
+ return jsMin(hi, jsMax(lo, v));
47
+ }
48
+
49
+ // -0 is tagged by the canonical layer, so the reference keeps it out of state.
50
+ double normalizeZero(double v) { return (v == 0.0 && std::signbit(v)) ? 0.0 : v; }
51
+
52
+ double mulFixed(const Config& c, double a, double b) {
53
+ return std::trunc((a * b) / c.fixedScale);
54
+ }
55
+
56
+ double sinQuarter(const Config& c, double xTurns) {
57
+ const double q = c.fixedScale / 4;
58
+ const double u = std::trunc((xTurns * c.fixedScale) / q);
59
+ const double u2 = mulFixed(c, u, u);
60
+ const double u3 = mulFixed(c, u2, u);
61
+ const double u5 = mulFixed(c, u3, u2);
62
+ return mulFixed(c, jsRound(1.5707963 * c.fixedScale), u) +
63
+ mulFixed(c, jsRound(-0.6459641 * c.fixedScale), u3) +
64
+ mulFixed(c, jsRound(0.0796926 * c.fixedScale), u5);
65
+ }
66
+
67
+ double clampOne(const Config& c, double v) {
68
+ if (v < -c.fixedScale) return -c.fixedScale;
69
+ if (v > c.fixedScale) return c.fixedScale;
70
+ return v;
71
+ }
72
+
73
+ double sinTurnsImpl(const Config& c, double turns) {
74
+ const double q = c.fixedScale / 4;
75
+ const double t = std::fmod(std::fmod(turns, c.fixedScale) + c.fixedScale, c.fixedScale);
76
+ if (t < q) return normalizeZero(clampOne(c, sinQuarter(c, t)));
77
+ if (t < 2 * q) return normalizeZero(clampOne(c, sinQuarter(c, 2 * q - t)));
78
+ if (t < 3 * q) return normalizeZero(clampOne(c, -sinQuarter(c, t - 2 * q)));
79
+ return normalizeZero(clampOne(c, -sinQuarter(c, c.fixedScale - t)));
80
+ }
81
+
82
+ // Math.PI is the nearest double to pi in both languages, so these constants are
83
+ // bit-identical to the reference's this.toFixed(Math.PI / 4) and friends.
84
+ constexpr double kPi = 3.141592653589793;
85
+
86
+ } // namespace
87
+
88
+ Config defaultConfig() { return Config{1000.0, -2147483648.0, 2147483647.0}; }
89
+
90
+ double toFixed(const Config& c, double value, bool& ok) {
91
+ if (!std::isfinite(value)) {
92
+ ok = false;
93
+ return 0.0;
94
+ }
95
+ return normalizeZero(saturate(jsRound(value * c.fixedScale), c.minFixed, c.maxFixed));
96
+ }
97
+
98
+ double fromFixed(const Config& c, double value, bool& ok) {
99
+ if (!isFixed(value)) {
100
+ ok = false;
101
+ return 0.0;
102
+ }
103
+ return value / c.fixedScale;
104
+ }
105
+
106
+ double add(const Config& c, double a, double b, bool& ok) {
107
+ if (!isFixed(a) || !isFixed(b)) {
108
+ ok = false;
109
+ return 0.0;
110
+ }
111
+ const double result = a + b;
112
+ if (result < c.minFixed || result > c.maxFixed) {
113
+ ok = false;
114
+ return 0.0;
115
+ }
116
+ return result;
117
+ }
118
+
119
+ double sub(const Config& c, double a, double b, bool& ok) {
120
+ if (!isFixed(a) || !isFixed(b)) {
121
+ ok = false;
122
+ return 0.0;
123
+ }
124
+ const double result = a - b;
125
+ if (result < c.minFixed || result > c.maxFixed) {
126
+ ok = false;
127
+ return 0.0;
128
+ }
129
+ return result;
130
+ }
131
+
132
+ // Over-2^53 semantic: the reference computes a*b as an IEEE double and
133
+ // truncates, so products beyond 2^53 lose low bits identically on every peer. We
134
+ // reproduce that by computing in double rather than int64 — a naive int64 port
135
+ // stays exact and therefore DISAGREES. Pinned by the mulWide vectors in
136
+ // core/tests/fixed-corpus.mjs, which use widened bounds so the result is not
137
+ // saturated and the difference is observable.
138
+ double mul(const Config& c, double a, double b, bool& ok) {
139
+ if (!isFixed(a) || !isFixed(b)) {
140
+ ok = false;
141
+ return 0.0;
142
+ }
143
+ return normalizeZero(saturate(std::trunc((a * b) / c.fixedScale), c.minFixed, c.maxFixed));
144
+ }
145
+
146
+ double div(const Config& c, double a, double b, bool& ok) {
147
+ if (!isFixed(a) || !isFixed(b) || b == 0.0) {
148
+ ok = false;
149
+ return 0.0;
150
+ }
151
+ return normalizeZero(saturate(std::trunc((a * c.fixedScale) / b), c.minFixed, c.maxFixed));
152
+ }
153
+
154
+ double saturatingAdd(const Config& c, double a, double b, bool& ok) {
155
+ if (!isFixed(a) || !isFixed(b)) {
156
+ ok = false;
157
+ return 0.0;
158
+ }
159
+ return saturate(a + b, c.minFixed, c.maxFixed);
160
+ }
161
+
162
+ double saturatingSub(const Config& c, double a, double b, bool& ok) {
163
+ if (!isFixed(a) || !isFixed(b)) {
164
+ ok = false;
165
+ return 0.0;
166
+ }
167
+ return saturate(a - b, c.minFixed, c.maxFixed);
168
+ }
169
+
170
+ double quantize(const Config& c, double value, double engine, bool& ok) {
171
+ (void)c;
172
+ if (!isFixed(value) || !isFixed(engine) || engine <= 0.0) {
173
+ ok = false;
174
+ return 0.0;
175
+ }
176
+ return std::trunc(value / engine) * engine;
177
+ }
178
+
179
+ double sqrtFixed(const Config& c, double value, bool& ok) {
180
+ if (!isFixed(value) || value < 0.0) {
181
+ ok = false;
182
+ return 0.0;
183
+ }
184
+ if (value == 0.0) return 0.0;
185
+ const double n = value * c.fixedScale;
186
+ double x = n;
187
+ double y = std::trunc((x + 1) / 2);
188
+ while (y < x) {
189
+ x = y;
190
+ y = std::trunc((x + std::trunc(n / x)) / 2);
191
+ }
192
+ return saturate(x, c.minFixed, c.maxFixed);
193
+ }
194
+
195
+ double integerSqrt(const Config& c, double value, bool& ok) {
196
+ (void)c;
197
+ if (!isSafeInteger(value) || value < 0.0) {
198
+ ok = false;
199
+ return 0.0;
200
+ }
201
+ if (value < 2.0) return value;
202
+ double low = 1.0;
203
+ double high = jsMin(value, 94906266.0);
204
+ while (low <= high) {
205
+ const double mid = low + std::trunc((high - low) / 2);
206
+ if (mid <= std::trunc(value / mid)) {
207
+ low = mid + 1;
208
+ } else {
209
+ high = mid - 1;
210
+ }
211
+ }
212
+ return high;
213
+ }
214
+
215
+ double absFixed(const Config& c, double value, bool& ok) {
216
+ if (!isFixed(value)) {
217
+ ok = false;
218
+ return 0.0;
219
+ }
220
+ return saturate(value < 0.0 ? -value : value, c.minFixed, c.maxFixed);
221
+ }
222
+
223
+ double sign(const Config& c, double value, bool& ok) {
224
+ if (!isFixed(value)) {
225
+ ok = false;
226
+ return 0.0;
227
+ }
228
+ if (value > 0.0) return c.fixedScale;
229
+ if (value < 0.0) return -c.fixedScale;
230
+ return 0.0;
231
+ }
232
+
233
+ double minFixed(const Config& c, double a, double b, bool& ok) {
234
+ (void)c;
235
+ if (!isFixed(a) || !isFixed(b)) {
236
+ ok = false;
237
+ return 0.0;
238
+ }
239
+ return a < b ? a : b;
240
+ }
241
+
242
+ double maxFixed(const Config& c, double a, double b, bool& ok) {
243
+ (void)c;
244
+ if (!isFixed(a) || !isFixed(b)) {
245
+ ok = false;
246
+ return 0.0;
247
+ }
248
+ return a > b ? a : b;
249
+ }
250
+
251
+ double lerp(const Config& c, double a, double b, double t, bool& ok) {
252
+ if (!isFixed(a) || !isFixed(b) || !isFixed(t)) {
253
+ ok = false;
254
+ return 0.0;
255
+ }
256
+ return saturate(a + std::trunc(((b - a) * t) / c.fixedScale), c.minFixed, c.maxFixed);
257
+ }
258
+
259
+ double mod(const Config& c, double a, double b, bool& ok) {
260
+ (void)c;
261
+ if (!isFixed(a) || !isFixed(b) || b == 0.0) {
262
+ ok = false;
263
+ return 0.0;
264
+ }
265
+ const double r = std::fmod(a, b);
266
+ return (r != 0.0 && ((r < 0.0) != (b < 0.0))) ? r + b : r;
267
+ }
268
+
269
+ double inverseLerp(const Config& c, double a, double b, double v, bool& ok) {
270
+ if (!isFixed(a) || !isFixed(b) || !isFixed(v)) {
271
+ ok = false;
272
+ return 0.0;
273
+ }
274
+ if (a == b) return 0.0;
275
+ return saturate(std::trunc(((v - a) * c.fixedScale) / (b - a)), c.minFixed, c.maxFixed);
276
+ }
277
+
278
+ double remap(const Config& c, double inMin, double inMax, double outMin, double outMax, double v, bool& ok) {
279
+ const double t = inverseLerp(c, inMin, inMax, v, ok);
280
+ if (!ok) return 0.0;
281
+ return lerp(c, outMin, outMax, t, ok);
282
+ }
283
+
284
+ double smoothstep(const Config& c, double edge0, double edge1, double x, bool& ok) {
285
+ const double raw = inverseLerp(c, edge0, edge1, x, ok);
286
+ if (!ok) return 0.0;
287
+ const double t = clamp(c, raw, 0.0, c.fixedScale, ok);
288
+ const double t2 = std::trunc((t * t) / c.fixedScale);
289
+ const double t3 = std::trunc((t2 * t) / c.fixedScale);
290
+ return saturate(3 * t2 - 2 * t3, c.minFixed, c.maxFixed);
291
+ }
292
+
293
+ double log2Fixed(const Config& c, double value, bool& ok) {
294
+ if (!isFixed(value) || value <= 0.0) {
295
+ ok = false;
296
+ return 0.0;
297
+ }
298
+ double e = 0.0;
299
+ double v = value;
300
+ const double one = c.fixedScale;
301
+ while (v >= 2 * one) {
302
+ v = std::trunc(v / 2);
303
+ e += 1;
304
+ }
305
+ while (v < one) {
306
+ v = v * 2;
307
+ e -= 1;
308
+ }
309
+ const double f = v - one;
310
+ const double c1 = jsRound(1.4273 * c.fixedScale);
311
+ const double c2 = jsRound(0.6018 * c.fixedScale);
312
+ const double c3 = jsRound(0.1745 * c.fixedScale);
313
+ const double poly = mulFixed(c, f, c1 - mulFixed(c, f, c2 - mulFixed(c, f, c3)));
314
+ return e * c.fixedScale + poly;
315
+ }
316
+
317
+ double logFixed(const Config& c, double value, bool& ok) {
318
+ const double l2 = log2Fixed(c, value, ok);
319
+ if (!ok) return 0.0;
320
+ const double ln2 = jsRound(0.6931472 * c.fixedScale);
321
+ return saturate(std::trunc((l2 * ln2) / c.fixedScale), c.minFixed, c.maxFixed);
322
+ }
323
+
324
+ double expFixed(const Config& c, double value, bool& ok) {
325
+ if (!isFixed(value)) {
326
+ ok = false;
327
+ return 0.0;
328
+ }
329
+ const double invLn2 = jsRound(1.4426950 * c.fixedScale);
330
+ const double y = std::trunc((value * invLn2) / c.fixedScale);
331
+ double yi = std::trunc(y / c.fixedScale);
332
+ double yf = y - yi * c.fixedScale;
333
+ if (yf < 0.0) {
334
+ yf += c.fixedScale;
335
+ yi -= 1;
336
+ }
337
+ const double b1 = jsRound(0.6931472 * c.fixedScale);
338
+ const double b2 = jsRound(0.2401596 * c.fixedScale);
339
+ const double b3 = jsRound(0.0521416 * c.fixedScale);
340
+ double result = c.fixedScale + mulFixed(c, yf, b1 + mulFixed(c, yf, b2 + mulFixed(c, yf, b3)));
341
+ if (yi >= 0.0) {
342
+ for (double i = 0; i < yi; i += 1) {
343
+ result = saturate(result * 2, c.minFixed, c.maxFixed);
344
+ }
345
+ } else {
346
+ for (double i = 0; i < -yi; i += 1) {
347
+ result = std::trunc(result / 2);
348
+ }
349
+ }
350
+ return saturate(result, c.minFixed, c.maxFixed);
351
+ }
352
+
353
+ double sinTurns(const Config& c, double turns, bool& ok) {
354
+ if (!isFixed(turns)) {
355
+ ok = false;
356
+ return 0.0;
357
+ }
358
+ return sinTurnsImpl(c, turns);
359
+ }
360
+
361
+ double cosTurns(const Config& c, double turns, bool& ok) {
362
+ if (!isFixed(turns)) {
363
+ ok = false;
364
+ return 0.0;
365
+ }
366
+ return sinTurnsImpl(c, turns + c.fixedScale / 4);
367
+ }
368
+
369
+ double tanTurns(const Config& c, double turns, bool& ok) {
370
+ const double s = sinTurns(c, turns, ok);
371
+ if (!ok) return 0.0;
372
+ const double co = cosTurns(c, turns, ok);
373
+ if (!ok) return 0.0;
374
+ return div(c, s, co, ok);
375
+ }
376
+
377
+ double atan2Turns(const Config& c, double y, double x, bool& ok) {
378
+ if (!isFixed(y) || !isFixed(x)) {
379
+ ok = false;
380
+ return 0.0;
381
+ }
382
+ if (x == 0.0 && y == 0.0) return 0.0;
383
+ const double ax = x < 0.0 ? -x : x;
384
+ const double ay = y < 0.0 ? -y : y;
385
+ const bool swap = ay > ax;
386
+ const double z = div(c, swap ? ax : ay, swap ? ay : ax, ok);
387
+ if (!ok) return 0.0;
388
+ const double k1 = toFixed(c, kPi / 4, ok);
389
+ const double k2 = toFixed(c, 0.2447, ok);
390
+ const double k3 = toFixed(c, 0.0663, ok);
391
+ if (!ok) return 0.0;
392
+
393
+ const double term1 = mul(c, k1, z, ok);
394
+ const double zc = sub(c, c.fixedScale, z, ok);
395
+ const double zz = mul(c, z, zc, ok);
396
+ const double inner = add(c, k2, mul(c, k3, z, ok), ok);
397
+ const double atanRad = add(c, term1, mul(c, zz, inner, ok), ok);
398
+ if (!ok) return 0.0;
399
+
400
+ double turn = mul(c, atanRad, toFixed(c, 1 / (2 * kPi), ok), ok);
401
+ if (!ok) return 0.0;
402
+ if (swap) {
403
+ turn = sub(c, c.fixedScale / 4, turn, ok);
404
+ if (!ok) return 0.0;
405
+ }
406
+
407
+ double full = 0.0;
408
+ if (x >= 0.0 && y >= 0.0) {
409
+ full = turn;
410
+ } else if (x < 0.0 && y >= 0.0) {
411
+ full = sub(c, c.fixedScale / 2, turn, ok);
412
+ } else if (x < 0.0 && y < 0.0) {
413
+ full = add(c, c.fixedScale / 2, turn, ok);
414
+ } else {
415
+ full = sub(c, c.fixedScale, turn, ok);
416
+ }
417
+ if (!ok) return 0.0;
418
+ return std::fmod(std::fmod(full, c.fixedScale) + c.fixedScale, c.fixedScale);
419
+ }
420
+
421
+ double atan2Signed(const Config& c, double y, double x, bool& ok) {
422
+ const double turn = atan2Turns(c, y, x, ok);
423
+ if (!ok) return 0.0;
424
+ const double half = c.fixedScale / 2;
425
+ return turn >= half ? turn - c.fixedScale : turn;
426
+ }
427
+
428
+ double wrapTurns(const Config& c, double t, bool& ok) {
429
+ if (!isFixed(t)) {
430
+ ok = false;
431
+ return 0.0;
432
+ }
433
+ return std::fmod(std::fmod(t, c.fixedScale) + c.fixedScale, c.fixedScale);
434
+ }
435
+
436
+ double angleDeltaTurns(const Config& c, double a, double b, bool& ok) {
437
+ if (!isFixed(a) || !isFixed(b)) {
438
+ ok = false;
439
+ return 0.0;
440
+ }
441
+ const double half = c.fixedScale / 2;
442
+ double d = std::fmod(std::fmod(b - a, c.fixedScale) + c.fixedScale, c.fixedScale);
443
+ if (d >= half) d -= c.fixedScale;
444
+ return d;
445
+ }
446
+
447
+ double turnsToRadians(const Config& c, double t, bool& ok) {
448
+ if (!isFixed(t)) {
449
+ ok = false;
450
+ return 0.0;
451
+ }
452
+ const double tau = toFixed(c, 2 * kPi, ok);
453
+ if (!ok) return 0.0;
454
+ return saturate(std::trunc((t * tau) / c.fixedScale), c.minFixed, c.maxFixed);
455
+ }
456
+
457
+ double radiansToTurns(const Config& c, double r, bool& ok) {
458
+ if (!isFixed(r)) {
459
+ ok = false;
460
+ return 0.0;
461
+ }
462
+ const double tau = toFixed(c, 2 * kPi, ok);
463
+ if (!ok) return 0.0;
464
+ return saturate(std::trunc((r * c.fixedScale) / tau), c.minFixed, c.maxFixed);
465
+ }
466
+
467
+ double hypot(const Config& c, double x, double y, bool& ok) {
468
+ if (!isFixed(x) || !isFixed(y)) {
469
+ ok = false;
470
+ return 0.0;
471
+ }
472
+ const double xx = mul(c, x, x, ok);
473
+ const double yy = mul(c, y, y, ok);
474
+ if (!ok) return 0.0;
475
+ return sqrtFixed(c, saturate(xx + yy, c.minFixed, c.maxFixed), ok);
476
+ }
477
+
478
+ Vec2 vec2(const Config& c, double x, double y, bool& ok) {
479
+ (void)c;
480
+ if (!isFixed(x) || !isFixed(y)) {
481
+ ok = false;
482
+ return Vec2{0, 0};
483
+ }
484
+ return Vec2{x, y};
485
+ }
486
+
487
+ Vec2 vec2Add(const Config& c, Vec2 a, Vec2 b, bool& ok) {
488
+ (void)ok;
489
+ return Vec2{saturate(a.x + b.x, c.minFixed, c.maxFixed), saturate(a.y + b.y, c.minFixed, c.maxFixed)};
490
+ }
491
+
492
+ Vec2 vec2Sub(const Config& c, Vec2 a, Vec2 b, bool& ok) {
493
+ const double x = saturatingSub(c, a.x, b.x, ok);
494
+ const double y = saturatingSub(c, a.y, b.y, ok);
495
+ return Vec2{x, y};
496
+ }
497
+
498
+ Vec2 vec2Scale(const Config& c, Vec2 v, double s, bool& ok) {
499
+ if (!isFixed(v.x) || !isFixed(v.y) || !isFixed(s)) {
500
+ ok = false;
501
+ return Vec2{0, 0};
502
+ }
503
+ const double x = mul(c, v.x, s, ok);
504
+ const double y = mul(c, v.y, s, ok);
505
+ return Vec2{x, y};
506
+ }
507
+
508
+ double vec2Dot(const Config& c, Vec2 a, Vec2 b, bool& ok) {
509
+ const double xx = mul(c, a.x, b.x, ok);
510
+ const double yy = mul(c, a.y, b.y, ok);
511
+ if (!ok) return 0.0;
512
+ return saturate(xx + yy, c.minFixed, c.maxFixed);
513
+ }
514
+
515
+ double vec2Cross(const Config& c, Vec2 a, Vec2 b, bool& ok) {
516
+ const double xy = mul(c, a.x, b.y, ok);
517
+ const double yx = mul(c, a.y, b.x, ok);
518
+ if (!ok) return 0.0;
519
+ return saturate(xy - yx, c.minFixed, c.maxFixed);
520
+ }
521
+
522
+ double vec2LengthSq(const Config& c, Vec2 v, bool& ok) {
523
+ const double xx = mul(c, v.x, v.x, ok);
524
+ const double yy = mul(c, v.y, v.y, ok);
525
+ if (!ok) return 0.0;
526
+ return saturate(xx + yy, c.minFixed, c.maxFixed);
527
+ }
528
+
529
+ double vec2Length(const Config& c, Vec2 v, bool& ok) { return hypot(c, v.x, v.y, ok); }
530
+
531
+ double vec2DistanceSq(const Config& c, Vec2 a, Vec2 b, bool& ok) {
532
+ const Vec2 d = vec2Sub(c, a, b, ok);
533
+ if (!ok) return 0.0;
534
+ return vec2LengthSq(c, d, ok);
535
+ }
536
+
537
+ double vec2Distance(const Config& c, Vec2 a, Vec2 b, bool& ok) {
538
+ const double dx = saturatingSub(c, a.x, b.x, ok);
539
+ const double dy = saturatingSub(c, a.y, b.y, ok);
540
+ if (!ok) return 0.0;
541
+ return hypot(c, dx, dy, ok);
542
+ }
543
+
544
+ Vec2 vec2Normalize(const Config& c, Vec2 v, bool& ok) {
545
+ const double len = hypot(c, v.x, v.y, ok);
546
+ if (!ok) return Vec2{0, 0};
547
+ if (len == 0.0) return Vec2{0, 0};
548
+ const double x = div(c, v.x, len, ok);
549
+ const double y = div(c, v.y, len, ok);
550
+ return Vec2{x, y};
551
+ }
552
+
553
+ Vec2 vec2Perp(const Config& c, Vec2 v, bool& ok) {
554
+ if (!isFixed(v.x) || !isFixed(v.y)) {
555
+ ok = false;
556
+ return Vec2{0, 0};
557
+ }
558
+ return Vec2{saturatingSub(c, 0, v.y, ok), v.x};
559
+ }
560
+
561
+ Vec2 vec2Negate(const Config& c, Vec2 v, bool& ok) {
562
+ if (!isFixed(v.x) || !isFixed(v.y)) {
563
+ ok = false;
564
+ return Vec2{0, 0};
565
+ }
566
+ const double x = saturatingSub(c, 0, v.x, ok);
567
+ const double y = saturatingSub(c, 0, v.y, ok);
568
+ return Vec2{x, y};
569
+ }
570
+
571
+ Vec2 vec2Rotate(const Config& c, Vec2 v, double turns, bool& ok) {
572
+ if (!isFixed(v.x) || !isFixed(v.y) || !isFixed(turns)) {
573
+ ok = false;
574
+ return Vec2{0, 0};
575
+ }
576
+ const double co = cosTurns(c, turns, ok);
577
+ const double si = sinTurns(c, turns, ok);
578
+ if (!ok) return Vec2{0, 0};
579
+ const double x = saturatingSub(c, mul(c, v.x, co, ok), mul(c, v.y, si, ok), ok);
580
+ const double y = saturatingAdd(c, mul(c, v.x, si, ok), mul(c, v.y, co, ok), ok);
581
+ return Vec2{x, y};
582
+ }
583
+
584
+ Vec2 vec2FromAngleTurns(const Config& c, double turns, double length, bool& ok) {
585
+ if (!isFixed(turns) || !isFixed(length)) {
586
+ ok = false;
587
+ return Vec2{0, 0};
588
+ }
589
+ const double x = mul(c, cosTurns(c, turns, ok), length, ok);
590
+ const double y = mul(c, sinTurns(c, turns, ok), length, ok);
591
+ return Vec2{x, y};
592
+ }
593
+
594
+ double vec2AngleTurns(const Config& c, Vec2 v, bool& ok) { return atan2Turns(c, v.y, v.x, ok); }
595
+
596
+ Vec2 vec2Lerp(const Config& c, Vec2 a, Vec2 b, double t, bool& ok) {
597
+ if (!isFixed(t)) {
598
+ ok = false;
599
+ return Vec2{0, 0};
600
+ }
601
+ return Vec2{
602
+ normalizeZero(a.x + std::trunc(((b.x - a.x) * t) / c.fixedScale)),
603
+ normalizeZero(a.y + std::trunc(((b.y - a.y) * t) / c.fixedScale)),
604
+ };
605
+ }
606
+
607
+ Vec3 vec3(const Config& c, double x, double y, double z, bool& ok) {
608
+ (void)c;
609
+ if (!isFixed(x) || !isFixed(y) || !isFixed(z)) {
610
+ ok = false;
611
+ return Vec3{0, 0, 0};
612
+ }
613
+ return Vec3{x, y, z};
614
+ }
615
+
616
+ Vec3 vec3Add(const Config& c, Vec3 a, Vec3 b, bool& ok) {
617
+ (void)ok;
618
+ return Vec3{
619
+ saturate(a.x + b.x, c.minFixed, c.maxFixed),
620
+ saturate(a.y + b.y, c.minFixed, c.maxFixed),
621
+ saturate(a.z + b.z, c.minFixed, c.maxFixed),
622
+ };
623
+ }
624
+
625
+ Vec3 vec3Sub(const Config& c, Vec3 a, Vec3 b, bool& ok) {
626
+ const double x = saturatingSub(c, a.x, b.x, ok);
627
+ const double y = saturatingSub(c, a.y, b.y, ok);
628
+ const double z = saturatingSub(c, a.z, b.z, ok);
629
+ return Vec3{x, y, z};
630
+ }
631
+
632
+ Vec3 vec3Scale(const Config& c, Vec3 v, double s, bool& ok) {
633
+ if (!isFixed(s)) {
634
+ ok = false;
635
+ return Vec3{0, 0, 0};
636
+ }
637
+ const double x = mul(c, v.x, s, ok);
638
+ const double y = mul(c, v.y, s, ok);
639
+ const double z = mul(c, v.z, s, ok);
640
+ return Vec3{x, y, z};
641
+ }
642
+
643
+ double vec3Dot(const Config& c, Vec3 a, Vec3 b, bool& ok) {
644
+ const double xx = mul(c, a.x, b.x, ok);
645
+ const double yy = mul(c, a.y, b.y, ok);
646
+ const double zz = mul(c, a.z, b.z, ok);
647
+ if (!ok) return 0.0;
648
+ return saturate(xx + yy + zz, c.minFixed, c.maxFixed);
649
+ }
650
+
651
+ Vec3 vec3Cross(const Config& c, Vec3 a, Vec3 b, bool& ok) {
652
+ const double x = sub(c, mul(c, a.y, b.z, ok), mul(c, a.z, b.y, ok), ok);
653
+ const double y = sub(c, mul(c, a.z, b.x, ok), mul(c, a.x, b.z, ok), ok);
654
+ const double z = sub(c, mul(c, a.x, b.y, ok), mul(c, a.y, b.x, ok), ok);
655
+ return Vec3{x, y, z};
656
+ }
657
+
658
+ double vec3LengthSq(const Config& c, Vec3 v, bool& ok) {
659
+ const double xx = mul(c, v.x, v.x, ok);
660
+ const double yy = mul(c, v.y, v.y, ok);
661
+ const double zz = mul(c, v.z, v.z, ok);
662
+ if (!ok) return 0.0;
663
+ return saturate(xx + yy + zz, c.minFixed, c.maxFixed);
664
+ }
665
+
666
+ double vec3Length(const Config& c, Vec3 v, bool& ok) {
667
+ const double lenSq = vec3LengthSq(c, v, ok);
668
+ if (!ok) return 0.0;
669
+ return sqrtFixed(c, lenSq, ok);
670
+ }
671
+
672
+ double vec3Distance(const Config& c, Vec3 a, Vec3 b, bool& ok) {
673
+ const Vec3 d = vec3Sub(c, a, b, ok);
674
+ if (!ok) return 0.0;
675
+ return vec3Length(c, d, ok);
676
+ }
677
+
678
+ Vec3 vec3Normalize(const Config& c, Vec3 v, bool& ok) {
679
+ const double len = vec3Length(c, v, ok);
680
+ if (!ok) return Vec3{0, 0, 0};
681
+ if (len == 0.0) return Vec3{0, 0, 0};
682
+ const double x = div(c, v.x, len, ok);
683
+ const double y = div(c, v.y, len, ok);
684
+ const double z = div(c, v.z, len, ok);
685
+ return Vec3{x, y, z};
686
+ }
687
+
688
+ Vec3 vec3Lerp(const Config& c, Vec3 a, Vec3 b, double t, bool& ok) {
689
+ if (!isFixed(t)) {
690
+ ok = false;
691
+ return Vec3{0, 0, 0};
692
+ }
693
+ const double x = lerp(c, a.x, b.x, t, ok);
694
+ const double y = lerp(c, a.y, b.y, t, ok);
695
+ const double z = lerp(c, a.z, b.z, t, ok);
696
+ return Vec3{x, y, z};
697
+ }
698
+
699
+ Vec3 vec3Negate(const Config& c, Vec3 v, bool& ok) {
700
+ const double x = saturatingSub(c, 0, v.x, ok);
701
+ const double y = saturatingSub(c, 0, v.y, ok);
702
+ const double z = saturatingSub(c, 0, v.z, ok);
703
+ return Vec3{x, y, z};
704
+ }
705
+
706
+ Quat quatFromYawTurns(const Config& c, double turns, bool& ok) {
707
+ if (!isFixed(turns)) {
708
+ ok = false;
709
+ return Quat{0, 0, 0, 0};
710
+ }
711
+ const double halfTurns = std::trunc(turns / 2);
712
+ return Quat{
713
+ 0.0,
714
+ 0.0,
715
+ normalizeZero(sinTurnsImpl(c, halfTurns)),
716
+ normalizeZero(sinTurnsImpl(c, halfTurns + c.fixedScale / 4)),
717
+ };
718
+ }
719
+
720
+ Quat quatLerp(const Config& c, Quat a, Quat b, double t, bool& ok) {
721
+ if (!isFixed(t)) {
722
+ ok = false;
723
+ return Quat{0, 0, 0, 0};
724
+ }
725
+ return Quat{
726
+ a.x + std::trunc(((b.x - a.x) * t) / c.fixedScale),
727
+ a.y + std::trunc(((b.y - a.y) * t) / c.fixedScale),
728
+ a.z + std::trunc(((b.z - a.z) * t) / c.fixedScale),
729
+ a.w + std::trunc(((b.w - a.w) * t) / c.fixedScale),
730
+ };
731
+ }
732
+
733
+ // The only operation with no integer assertion, so it can be handed a real or a
734
+ // NaN and must return one. Everything else asserts first.
735
+ double clamp(const Config& c, double value, double lo, double hi, bool& ok) {
736
+ (void)c;
737
+ (void)ok;
738
+ return jsMin(hi, jsMax(lo, value));
739
+ }
740
+
741
+ } // namespace rundot::fixedmath