-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathvolts.ts
2308 lines (2125 loc) · 79.3 KB
/
volts.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//#region imports
import Scene from 'Scene';
import Diagnostics from 'Diagnostics';
import Reactive from 'Reactive';
import Time from 'Time';
import Blocks from 'Blocks';
import CameraInfo from 'CameraInfo';
import Materials from 'Materials';
// 👇 may be dynamically imported using `require`
let Persistence: {
userScope: {
get: (s: string) => Promise<object>;
set: (s: string, o: Object) => Promise<boolean>;
remove: (s: string) => Promise<boolean>;
};
},
Multipeer: {};
/**
* Plugins are stored on `_plugins`
* `plugins` is a creator-facing interface
*/
const _plugins: { [key: string]: VoltsPlugin } = {};
export const plugins: {
oimo: typeof import('./oimo.plugin');
[key: string]: VoltsPlugin;
} = Object.defineProperties({} as any, {
oimo: { get: () => safeImportPlugins('oimo') },
});
/**
* @description Allows the dynamic import of Volts' plugins
* @see https://github.com/facebook/react-native/issues/6391#issuecomment-194581270
*/
function safeImportPlugins(name: string, version?: number | string): VoltsPlugin {
if (!_plugins[name]) {
const fileName = `${name}.plugin.js`;
try {
switch (name) {
case 'oimo':
_plugins['oimo'] = require('./oimo.plugin');
break;
default:
throw new Error(`Plugin name is undefined`);
}
if (version && version !== _plugins[name].VERSION)
report(
`Plugin versions for "${name}" do not match. Expected version: ${version}, but received "${_plugins[name].VERSION}". Please make sure you include a compatible version of "${name}" in your project.`,
).asIssue('error');
if (_plugins[name].onImport && !_plugins[name].onImport())
report(`Plugin "${name} onImport function failed. Please check with the Plugin's creator"`);
} catch (error) {
report(
`Could not find module "${name}". Please make sure you include the "${fileName}" file in your project.\n${error}`,
).asIssue('error');
}
}
return _plugins[name];
}
//#endregion
//#region types
export type PublicOnly<T> = Pick<T, keyof T>;
// https://github.com/microsoft/TypeScript/issues/26223#issuecomment-410642988
interface FixedLengthArray<T, L extends number> extends Array<T> {
'0': T;
length: L;
}
// https://stackoverflow.com/a/53808212
type IfEquals<T, U, Y = unknown, N = never> = (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2
? Y
: N;
interface VoltsPlugin {
VERSION: number | string;
onImport?: () => boolean;
[key: string]: any;
}
type Snapshot = {
[key: string]:
| ScalarSignal
| Vec2Signal
| VectorSignal
| PointSignal
| Vec4Signal
| StringSignal
| BoolSignal
| QuaternionSignal;
};
type getDimsOfSignal<S> = S extends Vec4Signal
? 'x4' | 'y4' | 'z4' | 'w4'
: S extends VectorSignal
? 'x3' | 'y3' | 'z3'
: S extends PointSignal
? 'x3' | 'y3' | 'z3'
: S extends Vec2Signal
? 'x2' | 'y2'
: S extends ScalarSignal
? 'x1'
: never;
type ObjectToSnapshotable<Obj> = {
[Property in keyof Obj as `${Obj[Property] extends ISignal
? `CONVERTED::${Property extends string ? Property : never}::${getDimsOfSignal<Obj[Property]>}::UUID` & string
: never}`]: Obj[Property] extends Vec2Signal | VectorSignal | PointSignal | Vec4Signal | QuaternionSignal
? ScalarSignal
: Obj[Property];
};
type SnapshotToVanilla<Obj> = {
[Property in keyof Obj]: Obj[Property] extends Vec2Signal
? Vector<2>
: Obj[Property] extends VectorSignal
? Vector<3>
: Obj[Property] extends PointSignal
? Vector<3>
: Obj[Property] extends Vec4Signal
? Vector<4>
: Obj[Property] extends ScalarSignal
? number
: Obj[Property] extends StringSignal
? string
: Obj[Property] extends BoolSignal
? boolean
: Obj[Property] extends QuaternionSignal
? Quaternion
: Obj[Property];
};
type ReactiveToVanilla<T> = T extends ScalarSignal
? number
: T extends StringSignal
? string
: T extends BoolSignal
? boolean
: any;
interface onFramePerformanceData {
fps: number;
delta: number;
frameCount: number;
}
/**
* @param elapsedTime The time elapsed since the timeout/interval was created
* @param count The amount of times the timeout/interval has been called.
* Note this is incremented after the function is called, so it will always be 0 for any timeout
* @param lastCall The last time the function was called. `let deltaBetweenCalls = elapsedTime-lastCall;`
* @param created The time (in elapsed Volts.World time, not UNIX) when the function was created
* @param onFramePerformanceData onFramePerformanceData corresponding to the frame when the function was called
*/
type TimedEventFunction = (
this: any,
timedEventFunctionArguments: { elapsedTime: number; count: number; lastCall: number; created: number },
onFramePerformanceData: onFramePerformanceData,
) => void;
interface TimedEvent {
recurring: boolean;
delay: number;
created: number;
lastCall: number;
cb: TimedEventFunction;
count: number;
onNext?: number;
}
//#endregion
//#region constants
const PI = 3.14159265359;
const TWO_PI = 6.28318530718;
//#endregion
//#region utils
//#region getUUIDv4
/**
* @see https://stackoverflow.com/a/2117523/14899497
*/
function getUUIDv4(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
//#endregion
//#region promiseAllConcurrent
/** @author atolkachiov */
/** @see https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b#gistcomment-3591045 */
const pAll = async (queue: Promise<any>[], concurrency: number, areFn: boolean) => {
let index = 0;
const results: any[] = [];
// Run a pseudo-thread
const execThread = async () => {
while (index < queue.length) {
const curIndex = index++;
// Use of `curIndex` is important because `index` may change after await is resolved
// @ts-expect-error
results[curIndex] = await (areFn ? queue[curIndex]() : queue[curIndex]);
}
};
// Start threads
const threads = [];
for (let thread = 0; thread < concurrency; thread++) {
threads.push(execThread());
}
await Promise.all(threads);
return results;
};
const promiseAllConcurrent =
(n: number, areFn: boolean) =>
(list: Promise<any>[]): Promise<any[]> =>
pAll(list, n, areFn);
//#endregion
//#region report
type LogLevels = 'log' | 'warn' | 'error' | 'throw';
interface Reporters {
asIssue: (lvl?: LogLevels) => void;
asBackwardsCompatibleDiagnosticsError: () => void;
}
type reportFn = ((...msg: string[] | [object]) => Reporters) & {
getSceneInfo: ({
getMaterials,
getTextures,
getIdentifiers,
getPositions,
}?: {
getMaterials?: boolean;
getTextures?: boolean;
getIdentifiers?: boolean;
getPositions?: boolean;
}) => Promise<string>;
};
const prettifyJSON = (obj: Object, spacing = 2) => JSON.stringify(obj, null, spacing);
// (!) doesn't get hoisted up
export const report: reportFn = function report(...msg: string[] | [object]): Reporters {
let message: any;
// provides a bit of backwards compatibility, keeps support at (112, 121]
const toLogLevel = (lvl: LogLevels, msg: string | object) => {
if (lvl === 'throw') {
throw msg;
} else {
Diagnostics[lvl] ? Diagnostics[lvl](msg) : Diagnostics.warn(msg + `\n\n[[logger not found: ${lvl}]]`);
}
};
if (msg.length > 1) {
message = msg.join('\n');
} else {
message = msg[0];
}
return {
asIssue: (lvl: LogLevels = 'warn') => {
message = new Error(`${message}`);
const info = `This issue arose during execution.\nIf you believe it's related to VOLTS itself, please report it as a Github issue here: https://github.com/tomaspietravallo/sparkar-volts/issues\nPlease make your report detailed (include this message too!), and if possible, include a package of your current project`;
message = `Message: ${message.message ? message.message : message}\n\nInfo: ${info}\n\nStack: ${
message.stack ? message.stack : undefined
}`;
toLogLevel(lvl, message);
},
asBackwardsCompatibleDiagnosticsError: () => {
Diagnostics.error
? Diagnostics.error(message)
: Diagnostics.warn
? Diagnostics.warn(message)
: Diagnostics.log(message);
},
};
} as any;
report.getSceneInfo = async function (
{ getMaterials, getTextures, getIdentifiers, getPositions } = {
getMaterials: true,
getTextures: true,
getIdentifiers: true,
getPositions: true,
},
): Promise<string> {
const Instance = World.getInstance(false);
const info: { [key: string]: any } = {};
if (Instance && Instance.loaded) {
const sceneData: { [key: string]: any } = {};
const keys = Object.keys(Instance.assets);
// loop over all assets, may include scene objects/textures/materials/others
for (let index = 0; index < keys.length; index++) {
const key = keys[index];
const element = Instance.assets[key];
const getElementData = async (e: any) => {
if (!e) return { warning: 'no-element-was-found' };
const data: { [key: string]: any } = {};
let mat, tex;
data['name'] = e.name;
data['hidden'] = e.hidden.pinLastValue();
if (getIdentifiers) data['identifier'] = e.identifier;
if (getPositions) {
data['position'] = Vector.fromSignal(e.transform.position).toString(5);
}
if (getMaterials || getTextures) {
mat = e.getMaterial ? (await e.getMaterial()) || {} : {};
if (getMaterials) data['material'] = mat.name || 'undefined';
if (getMaterials && getIdentifiers) data['material-id'] = mat.identifier || 'undefined';
}
if (getTextures) {
tex = mat && mat.getDiffuse ? (await mat.getDiffuse()) || {} : {};
data['texture'] = tex.name || 'undefined';
if (getIdentifiers) data['texture-id'] = tex.identifier || 'undefined';
}
return data;
};
if (Array.isArray(element) && element.length > 1) {
sceneData[key] = await promiseAllConcurrent(10, true)(element.map((e) => getElementData.bind(this, e)));
} else if (element) {
sceneData[key] = await getElementData(element[0]);
} else {
sceneData[key] = `obj[key] is possibly undefined. key: ${key}`;
}
}
info['scene'] = sceneData;
} else {
info['scene'] = 'no instance was found, or the current instance has not loaded yet';
}
info['modules'] = {
Persistence: !!Persistence,
Multipeer: !!Multipeer,
dynamicInstancing: !!Scene.create,
writableSignals: !!Reactive.scalarSignalSource,
};
return prettifyJSON(info);
};
//#endregion
//#region transformAcrossSpaces
/**
* @param vec The 3D VectorSignal to be transformed
* @param vecSpace The parent space in which `vec` is located
* @param targetSpace The parent space into which `vec` should be transformed into
* @returns A signal in the `targetSpace`, which in the absolute frame of reference, is equivalent to `vec` in it's `vecSpace`
*
* @example ```ts
* let firstObj: SceneObjectBase, secondarySceneObj: SceneObjectBase;
* secondarySceneObj.transform.position =
* transformAcrossSpaces(
* firstObj.transform.position,
* firstObj.parentWorldTransform,
* secondarySceneObj.parentWorldTransform
* )
* ```
*/
export function transformAcrossSpaces(
vec: VectorSignal | PointSignal,
vecParentSpace: TransformSignal,
targetParentSpace: TransformSignal,
): PointSignal {
if (!(vec && vec.z && vec.pinLastValue))
throw new Error(`@ transformAcrossSpaces: Argument vec is not defined, or is not a VectorSignal`);
if (!(vecParentSpace && vecParentSpace.inverse && vecParentSpace.pinLastValue))
throw new Error(`@ transformAcrossSpaces: Argument vecParentSpace is not defined, or is not a TransformSignal`);
if (!(targetParentSpace && targetParentSpace.inverse && targetParentSpace.pinLastValue))
throw new Error(`@ transformAcrossSpaces: Argument targetParentSpace is not defined, or is not a TransformSignal`);
return targetParentSpace.inverse().applyToPoint(vecParentSpace.applyToPoint(vec));
}
//#endregion
//#region randomBetween
export const randomBetween = (min: number, max: number): number => {
return Math.random() * (max - min) + min;
};
//#endregion
//#region HSVtoRGB
/**
* @see https://stackoverflow.com/a/54024653
*/
export function hsv2rgb(h: number, s: number, v: number): [number, number, number] {
h *= 360;
/* istanbul ignore next */
const f = (n: number, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);
return [f(5), f(3), f(1)];
}
//#endregion
//#region allBinaryOptions
export function allBinaryOptions(len: number, a: number, b: number): (typeof a | typeof b)[][] {
const binary: (typeof a | typeof b)[][] = [];
for (let index = 0; index < 2 ** len; index++) {
const binaryString = index.toString(2);
binary.push(
(
Array(len - binaryString.length)
.fill('0')
.join('') + binaryString
)
.split('')
.map((n) => (Number(n) ? a : b)),
);
}
return binary;
}
//#endregion
//#endregion
//#region World
export enum PRODUCTION_MODES {
'PRODUCTION' = 'PRODUCTION',
'DEV' = 'DEV',
'NO_AUTO' = 'NO_AUTO',
}
interface InternalSignals {
__volts__internal__time: number;
__volts__internal__focalDistance: number;
__volts__internal__screen: Vector<3>;
__volts__internal__screenSizePixels: Vector<2>;
}
interface Events<S extends Snapshot> {
load: (snapshot?: SnapshotToVanilla<S>) => void;
frameUpdate: (snapshot?: SnapshotToVanilla<S>, data?: onFramePerformanceData) => void;
[event: string]: (...args: any) => void;
}
interface InternalWorldData {
initPromise: () => Promise<void>;
loaded: boolean;
running: boolean;
events: Partial<{ [E in keyof Events<Snapshot>]: Events<Snapshot>[E][] }>;
timedEvents: TimedEvent[];
elapsedTime: number;
frameCount: number;
FLAGS: { stopTimeout: boolean; lockInternalSnapshotOverride: boolean };
formattedValuesToSnapshot: ObjectToSnapshotable<Snapshot>;
userFriendlySnapshot: SnapshotToVanilla<Snapshot> & InternalSignals;
quaternions: Map<string, boolean>;
Camera: Camera;
}
interface WorldConfig {
mode: keyof typeof PRODUCTION_MODES | `${number}x${number}`;
assets?: { [key: string]: Promise<any | any[]> };
snapshot?: Snapshot;
loadStates?: State<any> | State<any>[];
}
class VoltsWorld<WorldConfigParams extends WorldConfig> {
private static instance: VoltsWorld<any>;
private static userConfig: WorldConfig;
static subscriptions: Function[];
protected internalData: InternalWorldData;
public assets: {
[Prop in keyof WorldConfigParams['assets']]: WorldConfigParams['assets'][Prop] extends PromiseLike<infer C>
? C
: never;
};
public mode: keyof typeof PRODUCTION_MODES | `${number}x${number}`;
private constructor() {
this.mode = VoltsWorld.userConfig.mode;
// @ts-expect-error
this.assets = {};
this.internalData = {
initPromise: this.init.bind(this, VoltsWorld.userConfig.assets, VoltsWorld.userConfig.loadStates),
running: false,
loaded: false,
events: {},
elapsedTime: 0,
frameCount: 0,
timedEvents: [],
// @ts-ignore missing props are assigned at runtime
userFriendlySnapshot: {},
formattedValuesToSnapshot: {},
FLAGS: {
stopTimeout: false,
lockInternalSnapshotOverride: false,
},
quaternions: new Map<string, boolean>(),
Camera: null,
};
// Quaternion support needs the internalData.quaternion map
this.internalData.formattedValuesToSnapshot = this.signalsToSnapshot_able(VoltsWorld.userConfig.snapshot);
// Making the promise public makes it easier to test with Jest
// Using Object.define so it doesn't show on the type def & doesn't raise ts errors
Object.defineProperty(this, 'rawInitPromise', {
value: this.internalData.initPromise(),
enumerable: false,
writable: false,
configurable: false,
});
for (let index = 0; index < VoltsWorld.subscriptions.length; index++) {
VoltsWorld.subscriptions[index]();
}
}
// @todo add uglier but user-friendlier long-form type
static getInstance<WorldConfigParams extends WorldConfig>(
config?: WorldConfigParams | boolean,
): VoltsWorld<WorldConfigParams> {
if (config === false) return VoltsWorld.instance;
if (!VoltsWorld.instance) {
if (typeof config !== 'object' || config === null)
throw new Error(
`@ VoltsWorld.getInstance: 'config' was not provided, but is required when creating the first instance`,
);
if (!config.mode)
throw new Error(
`@ VoltsWorld.getInstance: 'config.mode' was not provided, but is required when creating the first instance`,
);
// @ts-expect-error
if (!Object.values(PRODUCTION_MODES).includes(config.mode) && config.mode.indexOf('x') === -1)
throw new Error(
`@ VoltsWorld.getInstance: 'config.mode' was provided, but was not valid.\n\nAvailable modes are: ${Object.values(
PRODUCTION_MODES,
)}`,
);
config.loadStates = config.loadStates || [];
Array.isArray(config.loadStates) ? config.loadStates : [config.loadStates];
config.assets = config.assets || {};
config.snapshot = config.snapshot || {};
VoltsWorld.userConfig = config;
VoltsWorld.instance = new VoltsWorld();
} else if (config) {
Diagnostics.warn(
`@ VoltsWorld.getInstance: 'config' was provided (attempted to create new instance) but there's already an instance running`,
);
}
return VoltsWorld.instance;
}
/** @description Use this function to run a fn when a new Instance gets created */
static subscribeToInstance(cb: () => void): boolean {
if (typeof cb === 'function') return !!VoltsWorld.subscriptions.push(cb);
return false;
}
static devClear() {
// const Instance = VoltsWorld.getInstance(false);
VoltsWorld.userConfig = undefined;
VoltsWorld.instance = undefined;
VoltsWorld.subscriptions = [];
}
private async init(assets: WorldConfig['assets'], states: State<any>[]): Promise<void> {
this.internalData.Camera = (await Scene.root.findFirst('Camera')) as Camera;
if (!this.internalData.FLAGS.lockInternalSnapshotOverride)
this.addToSnapshot({
__volts__internal__focalDistance: this.internalData.Camera.focalPlane.distance,
__volts__internal__time: Time.ms,
__volts__internal__screen: Scene.unprojectToFocalPlane(Reactive.point2d(0, 0)),
__volts__internal__screenSizePixels: CameraInfo.previewSize,
});
// (three internal keys are manually deleted on load)
this.internalData.FLAGS.lockInternalSnapshotOverride = true;
// load states
// States are automatically loaded when created
// @ts-ignore loadState is purposely not part of the type
const loadStateArr = await promiseAllConcurrent(10, true)(states.map((s: State<any>) => s.loadState));
const keys = Object.keys(assets);
const getAssets: any[] = await promiseAllConcurrent(10, false)(keys.map((n) => assets[n]));
for (let k = 0; k < keys.length; k++) {
if (!getAssets[k]) throw new Error(`@ Volts.World.init: Object(s) not found. Key: "${keys[k]}"`);
// @ts-ignore
// To be properly typed out. Unfortunately, i think loading everything at once with an array ([...keys.map((n) =>...) would make it very challenging...
// Might be best to ts-ignore or `as unknown` in this case
this.assets[keys[k]] = Array.isArray(getAssets[k])
? getAssets[k].sort((a, b) => {
return a.name.localeCompare(b.name);
})
: getAssets[k];
// .map(objBody=>{ return new Object3D(objBody) });
}
this.internalData.loaded = true;
if (this.mode !== PRODUCTION_MODES.NO_AUTO) this.run();
}
public run(): boolean {
if (this.internalData.running) return false;
this.internalData.FLAGS.stopTimeout = false;
this.internalData.running = true;
// Fun fact: Time.setTimeoutWithSnapshot will run even if the Studio is paused
// Meaning this would keep executing, along with any onFrame function
// For DEV purposes, the function will not execute if it detects the studio is on pause
// This won't be the case when the mode is set to PROD, in case some device has undocumented behavior within the margin of error (3 frames)
const lastThreeFrames: number[] = [];
let offset = 0;
const loop = () => {
Time.setTimeoutWithSnapshot(
this.internalData.formattedValuesToSnapshot as { [key: string]: any },
(_: number, snapshot: any) => {
//#region Snapshot
snapshot = this.formattedSnapshotToUserFriendly(snapshot);
this.internalData.userFriendlySnapshot = { ...this.internalData.userFriendlySnapshot, ...snapshot };
//#endregion
//#region Capture data & analytics
if (!lastThreeFrames[0]) offset = this.internalData.userFriendlySnapshot.__volts__internal__time || 0;
const delta =
(this.internalData.userFriendlySnapshot.__volts__internal__time || 0) -
offset -
this.internalData.elapsedTime;
const fps = Math.round((1000 / delta) * 10) / 10;
this.internalData.elapsedTime += delta;
if (lastThreeFrames.length > 2) {
lastThreeFrames[0] = lastThreeFrames[1];
lastThreeFrames[1] = lastThreeFrames[2];
lastThreeFrames[2] = this.internalData.userFriendlySnapshot.__volts__internal__time;
} else {
lastThreeFrames.push(this.internalData.userFriendlySnapshot.__volts__internal__time);
}
//#endregion
// For DEV purposes, the function will not execute if it detects the studio is on pause
if (
lastThreeFrames[0] === lastThreeFrames[1] &&
lastThreeFrames[1] === lastThreeFrames[2] &&
VoltsWorld.userConfig.mode !== PRODUCTION_MODES.PRODUCTION
)
return loop();
const run = () => {
const onFramePerformanceData = { fps, delta, frameCount: this.internalData.frameCount };
this.runTimedEvents(onFramePerformanceData);
this.emitEvent('frameUpdate', this.internalData.userFriendlySnapshot, onFramePerformanceData);
this.internalData.frameCount += 1;
if (!this.internalData.FLAGS.stopTimeout) return loop();
};
if (this.frameCount === 0) {
let loadReturn;
if (VoltsWorld.userConfig.mode !== PRODUCTION_MODES.NO_AUTO) {
// @ts-expect-error
delete this.internalData.formattedValuesToSnapshot['__volts__internal__screen']; // @ts-expect-error
delete this.internalData.formattedValuesToSnapshot['__volts__internal__screenSizePixels']; // @ts-expect-error
delete this.internalData.formattedValuesToSnapshot['__volts__internal__focalDistance'];
if (this.mode.indexOf('x') !== -1) {
this.mode = this.internalData.userFriendlySnapshot.__volts__internal__screenSizePixels.equals(
new Vector(this.mode.split('x').map((n) => Number(n))),
)
? 'DEV'
: 'PRODUCTION';
}
this.emitEvent('load', this.internalData.userFriendlySnapshot);
}
if (loadReturn && loadReturn.then) {
loadReturn.then(run);
} else {
run();
}
} else {
run();
}
},
0,
);
};
loop();
return true;
}
get loaded(): boolean {
return this.internalData.loaded;
}
get running(): boolean {
return this.internalData.running;
}
get frameCount(): number {
return this.internalData.frameCount;
}
get snapshot(): SnapshotToVanilla<WorldConfigParams['snapshot']> & { [key: string]: any } {
return this.internalData.userFriendlySnapshot;
}
/**
* @description Runs World.init. **This is NOT RECOMMENDED**. This function will not load new assets or states.
*/
public forceAssetReload(): Promise<void> {
return this.internalData.initPromise();
}
/**
* @description Freezes the World instance in time.
* @returns
*/
public stop({ clearTimedEvents } = { clearTimedEvents: false }): boolean {
if (!this.internalData.running) return false;
this.internalData.running = false;
if (clearTimedEvents) this.internalData.timedEvents = [];
this.internalData.FLAGS.stopTimeout = true;
return true;
}
/**
* @author Andrey Sitnik
* @param event The event name.
* @param args The arguments for listeners.
* @see https://github.com/ai/nanoevents
*/
public emitEvent(event: string, ...args: any[]): void {
const shouldBind = ['load', 'frameUpdate', 'internal'].some((e) => e === event);
const evts = this.internalData.events[event] || [];
for (let index = 0; index < evts.length; index++) {
const event = evts[index];
if (shouldBind) {
event.bind(this)(...args);
} else {
event(...args);
}
}
}
/**
* @author Andrey Sitnik
* @param event The event name.
* @param cb The listener function.
* @returns Unbind listener from event.
* @see https://github.com/ai/nanoevents
*/
public onEvent<K extends keyof Events<SnapshotToVanilla<WorldConfigParams['snapshot']> & { [key: string]: any }>>(
event: K,
cb: Events<SnapshotToVanilla<WorldConfigParams['snapshot']> & { [key: string]: any }>[K],
): () => void {
(this.internalData.events[event] = this.internalData.events[event] || []).push(cb);
return () => (this.internalData.events[event] = (this.internalData.events[event] || []).filter((i) => i !== cb));
}
public onNextTick(cb: () => void): { clear: () => void } {
return this.setTimedEvent(cb, { ms: 0, recurring: false, onNext: this.frameCount });
}
/**
* @description Creates a timeout that executes the function after a given number of milliseconds
* @param cb The function to be executed
* @param ms The amount of milliseconds to wait before calling the function
* @returns The `clear` function, with which you can clear the timeout, preventing any future executions
*/
public setTimeout(cb: TimedEventFunction, ms: number): { clear: () => void } {
// if (!this.internalData.running)
// Diagnostics.warn('Warning @ Volts.World.setTimeout: created a timeout while the current instance is not running');
return this.setTimedEvent(cb, { ms, recurring: false });
}
/**
* @description Creates an interval that executes the function every [X] milliseconds
* @param cb The function to be executed
* @param ms The amount of milliseconds to wait before calling the function
* @returns The `clear` function, with which you can clear the interval, preventing any future executions
*/
public setInterval(cb: TimedEventFunction, ms: number): { clear: () => void } {
// if (!this.internalData.running)
// Diagnostics.warn( 'Warning @ Volts.World.setInterval: created an interval while the current instance is not running', );
return this.setTimedEvent(cb, { ms, recurring: true });
}
/**
* @param cb The function to be called
* @param ms The amount of ms
* @param trailing Whether the debounce should trail or lead. False means the debounce will lead
* @see http://demo.nimius.net/debounce_throttle/
*/
public setDebounce<argTypes extends Array<any>>(
cb: (...args: argTypes) => void,
ms: number,
trailing = false,
): (...args: argTypes) => void {
// if (!this.internalData.running)
// Diagnostics.warn('Warning @ Volts.World.setDebounce: created a debounce while the current instance is not running', );
let timer: { clear: () => void };
if (trailing)
// trailing
return (...args: argTypes): void => {
timer && timer.clear();
timer = this.setTimeout(() => {
cb.apply(this, args);
}, ms);
};
// leading
return (...args: argTypes): void => {
if (!timer) {
cb.apply(this, args);
}
timer && timer.clear();
timer = this.setTimeout(() => {
timer = undefined;
}, ms);
};
}
protected setTimedEvent(
cb: TimedEventFunction,
{ ms, recurring, onNext }: { ms?: number; recurring?: boolean; onNext?: number },
): { clear: () => void } {
const event: TimedEvent = {
created: this.internalData.elapsedTime,
lastCall: this.internalData.elapsedTime,
count: 0,
delay: ms,
recurring,
cb,
onNext,
};
this.internalData.timedEvents.push(event);
return {
clear: () => (this.internalData.timedEvents = (this.internalData.timedEvents || []).filter((i) => i !== event)),
};
}
private runTimedEvents(onFramePerformanceData: onFramePerformanceData) {
this.internalData.timedEvents = this.internalData.timedEvents.sort(
(e1, e2) => e1.lastCall + e1.delay - (e2.lastCall + e2.delay),
);
let i = this.internalData.timedEvents.length;
while (i--) {
const event = this.internalData.timedEvents[i];
if (
(event.onNext !== undefined && event.onNext !== this.frameCount) ||
(event.onNext === undefined && event.lastCall + event.delay < this.internalData.elapsedTime)
) {
event.cb.apply(this, [
this.internalData.elapsedTime - event.created,
event.count,
event.lastCall,
event.created,
onFramePerformanceData,
]);
this.internalData.timedEvents[i].count++;
if (event.recurring) {
this.internalData.timedEvents[i].lastCall = this.internalData.elapsedTime;
} else {
this.internalData.timedEvents.splice(i, 1);
}
}
}
}
protected signalsToSnapshot_able<values extends Snapshot>(values: values): ObjectToSnapshotable<values> {
// The purpose of the prefix & suffix is to ensure any signal values added to the snapshot don't collide.
// Eg. were vec3 'V1' to be broken up into 'V1x' 'V1y' 'V1z', it'd collide with any signals named 'V1x' 'V1y' 'V1z'
// Here the names would get converted to 'CONVERTED::V1::x|y|z|w:[UUID]', later pieced back together into a number[]
// Hopefully reducing any possible error that might arise from the accordion needed to work together with subscribeWithSnapshot
const prefix = 'CONVERTED';
const suffix = getUUIDv4();
const getKey = (k: string, e: string) => `${prefix}::${k}::${e}::${suffix}`;
// @ts-ignore
const tmp: { [key: string]: any } = {};
const keys = Object.keys(values);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const signal: any = values[key]; // any used instead of 14 separate @ts-ignore s
if (!signal) throw new Error(`@ (static) signalsToSnapshot_able: value[key] is not defined. Key: "${key}"`);
if (signal.w) {
// vec4
tmp[getKey(key, 'w4')] = signal.w;
tmp[getKey(key, 'z4')] = signal.z;
tmp[getKey(key, 'y4')] = signal.y;
tmp[getKey(key, 'x4')] = signal.x;
if (signal.eulerAngles) this.internalData.quaternions.set(key, true);
} else if (signal.z) {
// vec3
tmp[getKey(key, 'z3')] = signal.z;
tmp[getKey(key, 'y3')] = signal.y;
tmp[getKey(key, 'x3')] = signal.x;
} else if (signal.y) {
// vec2
tmp[getKey(key, 'y2')] = signal.y;
tmp[getKey(key, 'x2')] = signal.x;
} else if (signal.xor || signal.concat || signal.pinLastValue) {
// bool // string // scalar, this very likely unintentionally catches any and all other signal types, even the ones that can't be snapshot'ed
tmp[getKey(key, 'x1')] = signal;
} else {
throw new Error(
`@ (static) signalsToSnapshot_able: The provided Signal is not defined or is not supported. Key: "${key}"\n\nPlease consider opening an issue/PR: https://github.com/tomaspietravallo/sparkar-volts/issues`,
);
}
}
// @ts-ignore
return tmp;
}
protected formattedSnapshotToUserFriendly(snapshot: ObjectToSnapshotable<Snapshot>): SnapshotToVanilla<Snapshot> {
let keys = Object.keys(snapshot);
const signals: { [key: string]: [number, string] } = {}; // name, dimension
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const parts: string[] = key.split('::');
if (parts.length !== 4 || parts[0] !== 'CONVERTED')
throw new Error(
`@ Volts.World.formattedSnapshotToUserFriendly: Signal is missing the correct prefix, or is missing parts. Key: ${key}. Parts: ${parts}`,
);
const name = parts[1];
// eslint-disable-line no-alert
const [component, dimension] = parts[2].split('');
const uuid = parts[3];
signals[name] = [Number(dimension), uuid];
}
keys = Object.keys(signals);
const result: { [key: string]: any } = {};
for (let i = 0; i < keys.length; i++) {
const name = keys[i];
const [dim, uuid] = signals[name];
if (!Number.isFinite(dim) || dim == 0 || dim > 4)
report(
`@ Volts.World.formattedSnapshotToUserFriendly: dimension of signals[name] not 1|2|3|4. Dim: ${dim}. Name: ${name}.\n\nKeys: ${keys}`,
).asIssue('throw');
const arr: any[] = [];
for (let index = 0; index < dim; index++) {
arr.push(snapshot[`CONVERTED::${name}::${Vector.components[index]}${dim}::${uuid}`]);
}
if (this.internalData.quaternions.has(name)) {
result[name] = new Quaternion(arr[3], arr[0], arr[1], arr[2]);
} else {
result[name] = dim >= 2 ? new Vector(arr) : arr[0];
}
}
return result;
}
public addToSnapshot(obj: Snapshot = {}): void {
if (
this.internalData.FLAGS.lockInternalSnapshotOverride &&
!Object.keys(obj).every((k) => k.indexOf('__volts__internal') === -1)
)
throw new Error('Cannot override internal key after the internal snapshot override has been locked');
this.internalData.formattedValuesToSnapshot = Object.assign(
this.internalData.formattedValuesToSnapshot,
this.signalsToSnapshot_able(obj),
);
}
public removeFromSnapshot(keys: string | string[]): void {
const keysToRemove = Array.isArray(keys) ? keys : [keys];
if (
this.internalData.FLAGS.lockInternalSnapshotOverride &&
!keysToRemove.every((k) => k.indexOf('__volts__internal') === -1)
)
throw new Error('Cannot remove internal key after the internal snapshot override has been locked');
const snapKeys = Object.keys(this.internalData.formattedValuesToSnapshot);
const matches = snapKeys.filter((k) => keysToRemove.indexOf(k.split('::')[1]) !== -1);
for (let index = 0; index < matches.length; index++) {
const match = matches[index];
delete this.internalData.formattedValuesToSnapshot[match];
}
}
/**
* @description Returns a 2D Vector representing the bottom right of the screen, in world space coordinates
*/
public getWorldSpaceScreenBounds(): Vector<3> {
if (!this.internalData.running) {
throw new Error(
`Vector.getWorldSpaceScreenBounds can only be called when there's a Volts.World instance running`,
);
}
// ask the spark team about this :D, at the time of writing (v119), this didn't output consistent results
return this.internalData.userFriendlySnapshot.__volts__internal__screen.copy().abs().mul(1, -1, 0);
}
}
VoltsWorld.subscriptions = [];
//#endregion
//#region Vector
type VectorArgRest<D extends number = any> = [number] | [number[]] | number[] | [Vector<D>];
interface NDVectorInstance<D extends number> {
values: number[];
readonly dimension: number;
add(...args: VectorArgRest): Vector<D>;
sub(...args: VectorArgRest): Vector<D>;